Skip to content

Commit

Permalink
Added on-commit handler to SearchField. Tweaked overall behaviour to …
Browse files Browse the repository at this point in the history
…properly react when hitting ESCAPE. Tweaked behaviour when committing to a value to close the popup.
  • Loading branch information
dlemmermann committed Aug 24, 2023
1 parent 44df496 commit 3ad5b0a
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ public void start(Stage primaryStage) throws Exception {

CountriesSearchField field = new CountriesSearchField();
field.getEditor().setPrefColumnCount(30);
field.addEventHandler(SearchField.SearchEvent.SUGGESTION_SELECTED, event -> {
System.out.println("A suggestion was selected! => " + event.getSelectedSuggestion());
});
field.setOnCommit(country-> System.out.println("on commit listener in demo was invoked, country = " + country));

Region regionLeft = new Region();
regionLeft.setPrefWidth(30);
Expand Down Expand Up @@ -83,7 +81,6 @@ public void start(Stage primaryStage) throws Exception {
vbox.setPadding(new Insets(20));

Scene scene = new Scene(vbox);
scene.focusOwnerProperty().addListener(it -> System.out.println("focus owner: " + scene.getFocusOwner()));

primaryStage.setTitle("Search Field");
primaryStage.setScene(scene);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ public void start(Stage primaryStage) throws Exception {
CSSFX.start();

Scene scene = new Scene(vbox);
scene.focusOwnerProperty().addListener(it -> System.out.println("focus owner: " + scene.getFocusOwner()));
primaryStage.setTitle("Tags Field");
primaryStage.setScene(scene);
primaryStage.sizeToScene();
Expand Down
136 changes: 84 additions & 52 deletions gemsfx/src/main/java/com/dlsc/gemsfx/SearchField.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@
import javafx.animation.Animation;
import javafx.animation.RotateTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
Expand All @@ -14,7 +27,13 @@
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
Expand All @@ -33,6 +52,7 @@
import java.util.Comparator;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Consumer;

/**
* The search field is a standard text field with auto suggest capabilities
Expand All @@ -50,7 +70,6 @@
* <h3>Matcher</h3>
*
* @param <T> the type of objects to work on
*
* @see #setSuggestionProvider(Callback)
* @see #setConverter(StringConverter)
* @see #setCellFactory(Callback)
Expand All @@ -68,8 +87,6 @@ public class SearchField<T> extends Control {

private final SearchFieldPopup<T> popup;

private final BooleanProperty shouldCommit = new SimpleBooleanProperty(this, "shouldCommit", false);

/**
* Constructs a new spotlight field. The field will set defaults for the
* matcher, the converter, the cell factory, and the comparator. It will
Expand All @@ -80,7 +97,7 @@ public class SearchField<T> extends Control {
public SearchField() {
getStyleClass().add(DEFAULT_STYLE_CLASS);

popup = new SearchFieldPopup<>(this, shouldCommit);
popup = new SearchFieldPopup<>(this);

editor.textProperty().bindBidirectional(textProperty());
editor.promptTextProperty().bindBidirectional(promptTextProperty());
Expand All @@ -93,14 +110,17 @@ public SearchField() {
commit();
if (getSelectedItem() == null) {
editor.setText("");
} else {
invokeCommitHandler();
}
}
});

addEventFilter(KeyEvent.ANY, evt -> {
addEventFilter(KeyEvent.KEY_RELEASED, evt -> {
if (evt.getCode().equals(KeyCode.RIGHT) || evt.getCode().equals(KeyCode.ENTER)) {
commit();
evt.consume();
invokeCommitHandler();
} else if (evt.getCode().equals(KeyCode.LEFT)) {
editor.positionCaret(Math.max(0, editor.getCaretPosition() - 1));
} else if (evt.getCode().equals(KeyCode.ESCAPE)) {
Expand Down Expand Up @@ -213,47 +233,77 @@ public T fromString(String s) {
}
});

searchService.setOnRunning(evt -> fireEvent(SearchEvent.createEventForText(SearchEvent.SEARCH_STARTED, searchService.getText())));
searchService.setOnRunning(evt -> fireEvent(new SearchEvent(SearchEvent.SEARCH_STARTED, searchService.getText())));

searchService.setOnSucceeded(evt -> {
update(searchService.getValue());
fireEvent(SearchEvent.createEventForText(SearchEvent.SEARCH_FINISHED, searchService.getText()));
fireEvent(new SearchEvent(SearchEvent.SEARCH_FINISHED, searchService.getText()));
});

searching.bind(searchService.runningProperty());
}

private void invokeCommitHandler() {
T selectedItem = getSelectedItem();
if (selectedItem != null) {
Consumer<T> onCommit = getOnCommit();
if (onCommit != null) {
onCommit.accept(selectedItem);
}
}
}

private boolean committing;

/**
* Makes the field commit to the currently selected item and updates
* the field to show the full text provided by the converter for the
* item.
* Makes the field commit to the currently selected item and updates the
* field to show the full text provided by the converter for the item.
* This method can be called multiple times. For a single event
* when the user explicitly commits to a value use the {@link #onCommitProperty()}.
*/
public void commit() {
if (shouldCommit.get()) {
System.out.println("committing");
committing = true;
try {
T selectedItem = getSelectedItem();
if (selectedItem != null) {
String text = getConverter().toString(selectedItem);
if (text != null) {
editor.setText(text);
editor.positionCaret(text.length());
} else {
clear();
}
committing = true;
try {
T selectedItem = getSelectedItem();
if (selectedItem != null) {
String text = getConverter().toString(selectedItem);
if (text != null) {
editor.setText(text);
editor.positionCaret(text.length());
} else {
clear();
}
} finally {
committing = false;
} else {
clear();
}
shouldCommit.set(false);

getProperties().put("committed", "");
} finally {
committing = false;
}
}

private final ObjectProperty<Consumer<T>> onCommit = new SimpleObjectProperty<>(this, "onCommit");

public final Consumer<T> getOnCommit() {
return onCommit.get();
}

/**
* A callback that gets invoked when the user has committed to the selected
* value. "Committing" means that the user has hit the ENTER key, or the RIGHT arrow,
* or the field has lost its focus.
*
* @return the commit handler
*/
public final ObjectProperty<Consumer<T>> onCommitProperty() {
return onCommit;
}

public void setOnCommit(Consumer<T> onCommit) {
this.onCommit.set(onCommit);
}

private class SearchEventHandlerProperty extends SimpleObjectProperty<EventHandler<SearchEvent>> {

private final EventType<SearchEvent> eventType;
Expand Down Expand Up @@ -462,6 +512,9 @@ public String getUserText() {
*/
public final void cancel() {
searchService.cancel();
getProperties().put("cancelled", "");
setSelectedItem(null);
setText("");
}

/**
Expand Down Expand Up @@ -723,9 +776,8 @@ public final BiFunction<T, String, Boolean> getMatcher() {
* with exactly the text typed by the user. Auto selection will cause the field to automatically complete
* the text typed by the user with the name of the match.
*
* @see #converterProperty()
*
* @return the function used for determining the best match in the suggestion list
* @see #converterProperty()
*/
public final ObjectProperty<BiFunction<T, String, Boolean>> matcherProperty() {
return matcher;
Expand Down Expand Up @@ -855,45 +907,24 @@ public static class SearchEvent extends Event {
*/
public static final EventType<SearchEvent> SEARCH_FINISHED = new EventType<>(Event.ANY, "SEARCH_FINISHED");

/**
* An event that gets fired when the user selects a suggestion.
*/
public static final EventType<SearchEvent> SUGGESTION_SELECTED = new EventType<>(Event.ANY, "SUGGESTION_SELECTED");

private final Object selectedSuggestion;

private final String text;

public static SearchEvent createEventForText(EventType<? extends SearchEvent> eventType, String text) {
return new SearchEvent(eventType, text, null);
}

public static SearchEvent createEventForSuggestion(Object suggestion) {
return new SearchEvent(SUGGESTION_SELECTED, null, suggestion);
}

private SearchEvent(EventType<? extends SearchEvent> eventType, String text, Object suggestion) {
public SearchEvent(EventType<? extends SearchEvent> eventType, String text) {
super(eventType);
this.text = text;
selectedSuggestion = suggestion;
}

public String getText() {
return text;
}

public Object getSelectedSuggestion() {
return selectedSuggestion;
}

@Override
public String toString() {
return new ToStringBuilder(this)
.append("eventType", eventType)
.append("target", target)
.append("consumed", consumed)
.append("text", text)
.append("selectedSuggestion", selectedSuggestion)
.toString();
}
}
Expand Down Expand Up @@ -1011,6 +1042,7 @@ protected void updateItem(T item, boolean empty) {
text3.setText("");
}
}

}

public final SearchFieldPopup<T> getPopup() {
Expand Down
9 changes: 7 additions & 2 deletions gemsfx/src/main/java/com/dlsc/gemsfx/TagsField.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import java.util.stream.Collectors;

/**
* This field is a specialisation of the {@link SearchField} control and supports
* This field is a specialization of the {@link SearchField} control and supports
* the additional feature of using the selected object as a tag. Tags are shown in front
* of the text input field. The control provides an observable list of the currently
* added tags. In addition, the field also allows the user to select one or more of
Expand Down Expand Up @@ -106,7 +106,7 @@ protected Skin<?> createDefaultSkin() {

@Override
public String getUserAgentStylesheet() {
return TagsField.class.getResource("tags-field.css").toExternalForm();
return Objects.requireNonNull(TagsField.class.getResource("tags-field.css")).toExternalForm();
}

/**
Expand All @@ -120,6 +120,7 @@ public void commit() {
addTags(selectedItem);
clear();
}
getProperties().put("committed", "");
}

/**
Expand Down Expand Up @@ -160,6 +161,7 @@ public final void setTags(ObservableList<T> tags) {
*
* @param values the value to add as a tag
*/
@SafeVarargs
public final void addTags(T... values) {
execute(new AddTagCommand(values));
}
Expand All @@ -169,6 +171,7 @@ public final void addTags(T... values) {
*
* @param values the tags to add
*/
@SafeVarargs
public final void removeTags(T... values) {
execute(new RemoveTagCommand(values));
}
Expand Down Expand Up @@ -229,6 +232,7 @@ private class AddTagCommand implements Command {

private T[] tags;

@SafeVarargs
public AddTagCommand(T... tags) {
this.tags = tags;
}
Expand All @@ -252,6 +256,7 @@ private class RemoveTagCommand implements Command {

private T[] tags;

@SafeVarargs
public RemoveTagCommand(T... tags) {
this.tags = tags;
}
Expand Down
21 changes: 15 additions & 6 deletions gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchFieldPopup.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
package com.dlsc.gemsfx.skins;

import com.dlsc.gemsfx.SearchField;
import javafx.beans.property.BooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.NodeOrientation;
import javafx.scene.Node;
Expand All @@ -26,11 +26,8 @@ public class SearchFieldPopup<T> extends PopupControl {

public static final String DEFAULT_STYLE_CLASS = "search-field-popup";

private final BooleanProperty shouldCommit;

public SearchFieldPopup(SearchField<T> searchField, BooleanProperty shouldCommit) {
public SearchFieldPopup(SearchField<T> searchField) {
this.searchField = Objects.requireNonNull(searchField);
this.shouldCommit = Objects.requireNonNull(shouldCommit);

minWidthProperty().bind(searchField.widthProperty());

Expand All @@ -40,6 +37,18 @@ public SearchFieldPopup(SearchField<T> searchField, BooleanProperty shouldCommit

getStyleClass().add(DEFAULT_STYLE_CLASS);

MapChangeListener<? super Object, ? super Object> l = change -> {
if (change.wasAdded()) {
if (change.getKey().equals("committed") || change.getKey().equals("cancelled")) {
hide();
searchField.getProperties().remove("committed");
searchField.getProperties().remove("cancelled");
}
}
};

searchField.getProperties().addListener(l);

searchField.addEventHandler(SearchField.SearchEvent.SEARCH_FINISHED, evt -> {
if ((!searchField.getSuggestions().isEmpty() || searchField.getPlaceholder() != null) && StringUtils.isNotBlank(searchField.getEditor().getText())) {

Expand Down Expand Up @@ -108,6 +117,6 @@ private void selectFirstSuggestion() {
}

protected Skin<?> createDefaultSkin() {
return new SearchFieldPopupSkin<>(this, shouldCommit);
return new SearchFieldPopupSkin<>(this);
}
}
Loading

0 comments on commit 3ad5b0a

Please sign in to comment.