ReactFX - "lazy" 实时搜索文本区域
ReactFX - "lazy" real-time search text area
这里是响应式编程的新手。
我正在尝试使用 ReactFX 在 JavaFX 中实现 "lazy" 实时搜索文本区域。这里的懒惰是指一旦用户停止输入一秒钟,它就会执行搜索。代码非常简单:
EventStream<Change<String>> textEvents = EventStreams.changesOf(textArea.textProperty())
.successionEnds(Duration.ofSeconds(1));
然后订阅该事件流,瞧瞧。
但我还希望它在用户按下 Enter 时立即执行搜索。我不确定如何以 "reactive" 的方式做到这一点。简单地对 Enter 键事件执行搜索会导致搜索触发两次(一次用于键事件,一次用于文本更改),所以这是我当前的解决方案:
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);
}
}
})
);
我试过使用 SuspendableEventStream.suspend()
认为它会 "drop" 所有未决事件,但它没有按预期工作,仍会发出未决事件:
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();
}
})
);
我怎样才能想到更好(更具反应性?)的解决方案?
未测试,但怎么样:
EventStream<KeyEvent> enterKeyPressedEvents = EventStreams.eventsOf(textArea, KeyEvent.KEY_PRESSED)
.filter(k -> k.getCode() == KeyCode.ENTER);
EventStream<Change<String>> textEvents = EventStreams.changesOf(textArea.textProperty())
.successionEnds(Duration.ofSeconds(1))
.filter(c -> ! isAdditionOfNewline(c, textArea.getCaratPosition()));
EventStreams.merge(enterKeyPressedEvents, textEvents)
.subscribe(o -> performSearch(textArea.getText()));
private boolean isAdditionOfNewline(Change<String> change, int caratPos) {
// TODO make sure this works as required
String oldText = change.getOldValue();
String newText = change.getNewValue();
if (oldText.length()+1 != newText.length() || caratPos == 0) {
return false ;
}
return oldText.equals(newText.substring(0, caratPos-1) + newText.substring(caratPos));
}
这是一个解决方案。此解决方案的关键部分是观察 flatMap
内的文本变化,它具有 "resetting" 文本流变化的效果。
import java.time.Duration;
import java.util.function.Function;
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)
.hook(KeyEvent::consume);
EventStream<?> searchImpulse = enterPresses.withDefaultEvent(null) // emit an event even before Enter is pressed
.flatMap(x -> {
EventStream<?> edits = EventStreams.changesOf(area.textProperty())
.successionEnds(Duration.ofSeconds(1));
return ((x == null) ? edits : edits.withDefaultEvent(null))
.map(Function.identity()); // just to get the proper type of the result
});
searchImpulse.subscribe(x -> System.out.println("Search now!"));
stage.setScene(new Scene(area));
stage.show();
}
}
这是另一种解决方案。这个计算 Enter 按下次数,并且仅当 Enter 计数在此期间未更改时才让编辑事件触发搜索。
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 AutoSearch2 extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
TextArea area = new TextArea();
EventStream<KeyEvent> enterPressed = EventStreams.eventsOf(area, KeyEvent.KEY_PRESSED)
.filter(k -> k.getCode() == KeyCode.ENTER)
.hook(KeyEvent::consume);
EventStream<Long> enterCount = enterPressed.accumulate(0L, (n, k) -> n + 1)
.withDefaultEvent(0L);
EventStream<Long> delayedEdits = enterCount.emitOnEach(EventStreams.changesOf(area.textProperty()))
.successionEnds(Duration.ofSeconds(1));
// check that the delayed edit event still has the current value of the Enter counter
EventStream<?> validEdits = enterCount.emitBothOnEach(delayedEdits)
.filter(cd -> cd.test((current, delayed) -> delayed == current));
EventStream<?> searchImpulse = EventStreams.merge(enterPressed, validEdits);
searchImpulse.subscribe(x -> System.out.println("Search now!"));
stage.setScene(new Scene(area));
stage.show();
}
}
这里是响应式编程的新手。
我正在尝试使用 ReactFX 在 JavaFX 中实现 "lazy" 实时搜索文本区域。这里的懒惰是指一旦用户停止输入一秒钟,它就会执行搜索。代码非常简单:
EventStream<Change<String>> textEvents = EventStreams.changesOf(textArea.textProperty())
.successionEnds(Duration.ofSeconds(1));
然后订阅该事件流,瞧瞧。
但我还希望它在用户按下 Enter 时立即执行搜索。我不确定如何以 "reactive" 的方式做到这一点。简单地对 Enter 键事件执行搜索会导致搜索触发两次(一次用于键事件,一次用于文本更改),所以这是我当前的解决方案:
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);
}
}
})
);
我试过使用 SuspendableEventStream.suspend()
认为它会 "drop" 所有未决事件,但它没有按预期工作,仍会发出未决事件:
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();
}
})
);
我怎样才能想到更好(更具反应性?)的解决方案?
未测试,但怎么样:
EventStream<KeyEvent> enterKeyPressedEvents = EventStreams.eventsOf(textArea, KeyEvent.KEY_PRESSED)
.filter(k -> k.getCode() == KeyCode.ENTER);
EventStream<Change<String>> textEvents = EventStreams.changesOf(textArea.textProperty())
.successionEnds(Duration.ofSeconds(1))
.filter(c -> ! isAdditionOfNewline(c, textArea.getCaratPosition()));
EventStreams.merge(enterKeyPressedEvents, textEvents)
.subscribe(o -> performSearch(textArea.getText()));
private boolean isAdditionOfNewline(Change<String> change, int caratPos) {
// TODO make sure this works as required
String oldText = change.getOldValue();
String newText = change.getNewValue();
if (oldText.length()+1 != newText.length() || caratPos == 0) {
return false ;
}
return oldText.equals(newText.substring(0, caratPos-1) + newText.substring(caratPos));
}
这是一个解决方案。此解决方案的关键部分是观察 flatMap
内的文本变化,它具有 "resetting" 文本流变化的效果。
import java.time.Duration;
import java.util.function.Function;
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)
.hook(KeyEvent::consume);
EventStream<?> searchImpulse = enterPresses.withDefaultEvent(null) // emit an event even before Enter is pressed
.flatMap(x -> {
EventStream<?> edits = EventStreams.changesOf(area.textProperty())
.successionEnds(Duration.ofSeconds(1));
return ((x == null) ? edits : edits.withDefaultEvent(null))
.map(Function.identity()); // just to get the proper type of the result
});
searchImpulse.subscribe(x -> System.out.println("Search now!"));
stage.setScene(new Scene(area));
stage.show();
}
}
这是另一种解决方案。这个计算 Enter 按下次数,并且仅当 Enter 计数在此期间未更改时才让编辑事件触发搜索。
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 AutoSearch2 extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
TextArea area = new TextArea();
EventStream<KeyEvent> enterPressed = EventStreams.eventsOf(area, KeyEvent.KEY_PRESSED)
.filter(k -> k.getCode() == KeyCode.ENTER)
.hook(KeyEvent::consume);
EventStream<Long> enterCount = enterPressed.accumulate(0L, (n, k) -> n + 1)
.withDefaultEvent(0L);
EventStream<Long> delayedEdits = enterCount.emitOnEach(EventStreams.changesOf(area.textProperty()))
.successionEnds(Duration.ofSeconds(1));
// check that the delayed edit event still has the current value of the Enter counter
EventStream<?> validEdits = enterCount.emitBothOnEach(delayedEdits)
.filter(cd -> cd.test((current, delayed) -> delayed == current));
EventStream<?> searchImpulse = EventStreams.merge(enterPressed, validEdits);
searchImpulse.subscribe(x -> System.out.println("Search now!"));
stage.setScene(new Scene(area));
stage.show();
}
}