Skip to content

Commit

Permalink
Add search history persistence feature to SearchTextField
Browse files Browse the repository at this point in the history
This update introduces a new feature to the SearchTextField component that allows for the history to be stored and loaded from user preferences. Using a new `preferencesId` property, the search history is now persistently stored across application sessions when `storingHistory` is set to true. Duplication and size limit of the history entries are properly handled to ensure a consistent user experience.
  • Loading branch information
dlemmermann committed May 10, 2024
1 parent 917736f commit 89cc9c6
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ public class SearchTextFieldApp extends Application {
public void start(Stage primaryStage) throws Exception {

field1 = new SearchTextField();
field1.setStoringHistory(true);
field1.setPreferencesId("standard-field");
field1.setCellFactory(param -> new RemovableListCell<>((listView, item) -> field1.removeHistory(item)));

SearchTextField field2 = new SearchTextField(true);
field2.setStoringHistory(true);
field2.setPreferencesId("round-field");

Label label = new Label("Max History Size:");
Spinner<Integer> maxHistorySizeSpinner = new Spinner<>(5, 50, 10, 5);
Expand Down
137 changes: 108 additions & 29 deletions gemsfx/src/main/java/com/dlsc/gemsfx/SearchTextField.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
package com.dlsc.gemsfx;

import com.dlsc.gemsfx.skins.SearchTextFieldHistoryPopup;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
Expand All @@ -35,6 +30,7 @@
import java.util.List;
import java.util.Objects;
import java.util.logging.Logger;
import java.util.prefs.Preferences;

/**
* A custom text field specifically designed for search functionality. This class enhances a text field with features
Expand All @@ -49,20 +45,30 @@
*/
public class SearchTextField extends CustomTextField {

private static final Logger LOG = Logger.getLogger(SearchTextField.class.getName());

private static final int DEFAULT_MAX_HISTORY_SIZE = 30;
private static final boolean ENABLE_HISTORY_POPUP = true;
private static final boolean DEFAULT_ADD_HISTORY_ON_ENTER = true;
private static final PseudoClass DISABLED_POPUP_PSEUDO_CLASS = PseudoClass.getPseudoClass("disabled-popup");
private static final PseudoClass HISTORY_POPUP_SHOWING_PSEUDO_CLASS = PseudoClass.getPseudoClass("history-popup-showing");

private final Logger LOG = Logger.getLogger(SearchTextField.class.getName());
private SearchTextFieldHistoryPopup historyPopup;
private final StackPane searchIconWrapper;

/**
* Constructs a new text field customized for search operations.
*/
public SearchTextField() {
this(false);
}

/**
* Constructs a new text field customized for search operations. The look and feel can be
* adjusted to feature rounded corners / sides.
*
* @param round if true the sides of the field will be round
*/
public SearchTextField(boolean round) {
if (round) {
getStyleClass().add("round");
Expand All @@ -83,6 +89,48 @@ public SearchTextField(boolean round) {

addEventHandlers();
addPropertyListeners();

getUnmodifiableHistory().addListener((Observable it) -> {
if (isStoringHistory()) {
String id = getPreferencesId();
if (StringUtils.isNotBlank(id)) {
storeHistory();
} else {
throw new UnsupportedOperationException("Cannot store history, the preferences ID is empty");
}
}
});

InvalidationListener loadHistoryListener = it -> {
if (isStoringHistory()) {
loadHistory();
}
};

storingHistoryProperty().addListener(loadHistoryListener);
preferencesIdProperty().addListener(loadHistoryListener);
}

private void storeHistory() {
Preferences preferences = getPreferences();
if (preferences != null) {
preferences.put("search-items", String.join(",", getUnmodifiableHistory()));
}
}

private void loadHistory() {
Preferences preferences = getPreferences();
if (preferences != null) {
history.setAll(preferences.get("search-items", "").split(","));
}
}

private Preferences getPreferences() {
String preferencesId = getPreferencesId();
if (StringUtils.isNotBlank(preferencesId)) {
return Preferences.userNodeForPackage(SearchTextField.class).node(preferencesId);
}
return null;
}

private void addEventHandlers() {
Expand Down Expand Up @@ -121,8 +169,9 @@ private void addPropertyListeners() {
LOG.warning("Max history size must be greater than or equal to 0. ");
}

if (history.size() > getSafetyMaxHistorySize()) {
history.remove(getSafetyMaxHistorySize(), history.size());
int max = Math.max(0, getMaxHistorySize());
if (history.size() > max) {
history.remove(max, history.size());
}
});
}
Expand Down Expand Up @@ -156,10 +205,8 @@ private StackPane createLeftNode() {
return searchIconWrapper;
}

/**
/*
* Handles the click event on the icon wrapper of the search text field.
*
* @param event the mouse event triggered by the click
*/
private void clickIconWrapperHandler(MouseEvent event) {
if (!isFocused()) {
Expand Down Expand Up @@ -198,7 +245,8 @@ public String getUserAgentStylesheet() {
private final ObservableList<String> history = FXCollections.observableArrayList();

/**
* Sets the history of the search text field.
* Sets the history of the search text field. The given list of Strings will be processed to guarantee unique
* entries.
*
* @param history the list of strings representing the history
*/
Expand All @@ -207,7 +255,7 @@ public final void setHistory(List<String> history) {
}

/**
* Adds the given item to the history.
* Adds the given item to the history. The method ensures that duplicates will not be added.
*
* @param item the item to add
*/
Expand All @@ -216,8 +264,10 @@ public final void addHistory(String item) {
history.remove(item);
history.add(0, item);
}
if (history.size() > getSafetyMaxHistorySize()) {
history.remove(getSafetyMaxHistorySize(), history.size());

int max = Math.max(0, getMaxHistorySize());
if (history.size() > max) {
history.remove(max, history.size());
}
}

Expand Down Expand Up @@ -404,25 +454,54 @@ public final ReadOnlyBooleanProperty historyPopupShowingProperty() {
return historyPopupShowing.getReadOnlyProperty();
}

private final StringProperty preferencesId = new SimpleStringProperty(this, "preferencesId");

public final String getPreferencesId() {
return preferencesId.get();
}

/**
* Converts a given list of strings to a unique list of strings. Filters out empty strings.
* Stores an ID for the field used for persisting the search history.
*
* @param history the list of strings to convert
* @return the converted unique list of strings
* @return the preferences id for the field
*/
private List<String> convertToUniqueList(List<String> history) {
return history.stream().distinct().filter(StringUtils::isNotEmpty).limit(getSafetyMaxHistorySize()).toList();
public final StringProperty preferencesIdProperty() {
return preferencesId;
}

public final void setPreferencesId(String id) {
this.preferencesId.set(id);
}

// storing size

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

public final boolean isStoringHistory() {
return storingHistory.get();
}

/**
* Returns the maximum size of the history list that ensures safety.
* If the value of maxHistorySize is negative, 0 is returned.
* Otherwise, the value of maxHistorySize is returned.
* Determines if the field's search history will be stored in the user preferences, so that the
* items will appear automatically next time the application is run.
*
* @return the safety maximum history size
* @return true if the search history of the field will be persisted
*/
public final int getSafetyMaxHistorySize() {
return Math.max(0, getMaxHistorySize());
public final BooleanProperty storingHistoryProperty() {
return storingHistory;
}

public final void setStoringHistory(boolean storingHistory) {
this.storingHistory.set(storingHistory);
}

/**
* Converts a given list of strings to a unique list of strings. Filters out empty strings.
*
* @param history the list of strings to convert
* @return the converted unique list of strings
*/
private List<String> convertToUniqueList(List<String> history) {
return history.stream().distinct().filter(StringUtils::isNotEmpty).limit(Math.max(0, getMaxHistorySize())).toList();
}
}

0 comments on commit 89cc9c6

Please sign in to comment.