My application contains a TextField and a ListView. The TextField allows users to enter search terms that will filter the contents of the ListView as they type.
The filtering process will match several fields within each DataItem in the ListView and return the results if any of them match.
What I want to do, however, is have those results prioritize items that match one particular field over the others.
For example, in the MCVE below, I have two items: Computer and Paper. The Computer item has a keyword for "paper," so searching for "paper" should return Computer as a result.
However, since I also have an item called Paper, the search should return Paper at the top of the list. In the MCVE, though, the results are still alphabetized:
Question: How would I go about ensuring any matches to the DataItem.name are listed above matches to a DataItem.keywords?
EDIT: Entering "pap" in the search field should also return "Paper" at the top, followed by the remaining matches, as the partial search term partially matches the DataItem name.
MCVE
DataItem.java:
import java.util.List;
public class DataItem {
// Instance properties
private final IntegerProperty id = new SimpleIntegerProperty();
private final StringProperty name = new SimpleStringProperty();
private final StringProperty description = new SimpleStringProperty();
// List of search keywords
private final ObjectProperty<List<String>> keywords = new SimpleObjectProperty<>();
public DataItem(int id, String name, String description, List<String> keywords) {
this.id.set(id);
this.name.set(name);
this.description.set(description);
this.keywords.set(keywords);
}
/**
* Creates a space-separated String of all the keywords; used for filtering later
*/
public String getKeywordsString() {
StringBuilder sb = new StringBuilder();
for (String keyword : keywords.get()) {
sb.append(keyword).append(" ");
}
return sb.toString();
}
public int getId() {
return id.get();
}
public IntegerProperty idProperty() {
return id;
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public String getDescription() {
return description.get();
}
public StringProperty descriptionProperty() {
return description;
}
public List<String> getKeywords() {
return keywords.get();
}
public ObjectProperty<List<String>> keywordsProperty() {
return keywords;
}
@Override
public String toString() {
return name.get();
}
}
Main.java:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class Main extends Application {
// TextField used for filtering the ListView
TextField txtSearch = new TextField();
// ListView to hold our DataItems
ListView<DataItem> dataItemListView = new ListView<>();
// The ObservableList of DataItems
ObservableList<DataItem> dataItems;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Add the search field and ListView to the layout
root.getChildren().addAll(txtSearch, dataItemListView);
// Build the dataItems List
dataItems = FXCollections.observableArrayList(buildDataItems());
// Add the filter logic
addSearchFilter();
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
/**
* Adds the functionality to filter the list dynamically as search terms are entered
*/
private void addSearchFilter() {
// Wrap the dataItems list in a filtered list, initially showing all items, alphabetically
FilteredList<DataItem> filteredList = new FilteredList<>(
dataItems.sorted(Comparator.comparing(DataItem::getName)));
// Add the predicate to filter the list whenever the search field changes
txtSearch.textProperty().addListener((observable, oldValue, newValue) ->
filteredList.setPredicate(dataItem -> {
// Clear any selection already present
dataItemListView.getSelectionModel().clearSelection();
// If the search field is empty, show all DataItems
if (newValue == null || newValue.isEmpty()) {
return true;
}
// Compare the DataItem's name and keywords with the search query (ignoring case)
String query = newValue.toLowerCase();
if (dataItem.getName().toLowerCase().contains(query)) {
// DataItem's name contains the search query
return true;
} else {
// Otherwise check if any of the search terms match those in the DataItem's keywords
// We split the query by space so we can match DataItems with multiple keywords
String[] searchTerms = query.split(" ");
boolean match = false;
for (String searchTerm : searchTerms) {
match = dataItem.getKeywordsString().toLowerCase().contains(searchTerm);
}
return match;
}
}));
// Wrap the filtered list in a SortedList
SortedList<DataItem> sortedList = new SortedList<>(filteredList);
// Update the ListView
dataItemListView.setItems(sortedList);
}
/**
* Generates a list of sample products
*/
private List<DataItem> buildDataItems() {
List<DataItem> dataItems = new ArrayList<>();
dataItems.add(new DataItem(
1, "School Supplies", "Learn things.",
Arrays.asList("pens", "pencils", "paper", "eraser")));
dataItems.add(new DataItem(
2, "Computer", "Do some things",
Arrays.asList("paper", "cpu", "keyboard", "monitor")));
dataItems.add(new DataItem(
3, "Keyboard", "Type things",
Arrays.asList("keys", "numpad", "input")));
dataItems.add(new DataItem(
4, "Printer", "Print things.",
Arrays.asList("paper", "ink", "computer")));
dataItems.add(new DataItem(
5, "Paper", "Report things.",
Arrays.asList("write", "printer", "notebook")));
return dataItems;
}
}
