From 58a53bcfb6e5dbd75d5d2384bdda253e18fb187d Mon Sep 17 00:00:00 2001 From: leewyatt Date: Sat, 11 May 2024 01:41:46 +0900 Subject: [PATCH] Refactor SearchTextField with SearchHistorySupport interface Updated SearchTextField to implement SearchHistorySupport interface, improving modularity and extendability. Removed SearchTextFieldHistoryPopup in favor of more generic SearchHistoryPopup. Renamed related popup skin class and updated references accordingly. Also, created new 'search-history-popup.css' for styling and cleared some unused styles in 'search-text-field.css'. --- .../com/dlsc/gemsfx/SearchHistorySupport.java | 181 ++++++++++++++++++ .../java/com/dlsc/gemsfx/SearchTextField.java | 50 ++--- .../dlsc/gemsfx/skins/SearchHistoryPopup.java | 35 ++++ ...pSkin.java => SearchHistoryPopupSkin.java} | 30 +-- .../skins/SearchTextFieldHistoryPopup.java | 35 ---- .../com/dlsc/gemsfx/search-history-popup.css | 65 +++++++ .../com/dlsc/gemsfx/search-text-field.css | 63 +----- 7 files changed, 324 insertions(+), 135 deletions(-) create mode 100644 gemsfx/src/main/java/com/dlsc/gemsfx/SearchHistorySupport.java create mode 100644 gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchHistoryPopup.java rename gemsfx/src/main/java/com/dlsc/gemsfx/skins/{SearchTextFieldHistoryPopupSkin.java => SearchHistoryPopupSkin.java} (62%) delete mode 100644 gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchTextFieldHistoryPopup.java create mode 100644 gemsfx/src/main/resources/com/dlsc/gemsfx/search-history-popup.css diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/SearchHistorySupport.java b/gemsfx/src/main/java/com/dlsc/gemsfx/SearchHistorySupport.java new file mode 100644 index 00000000..e9b51d6c --- /dev/null +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/SearchHistorySupport.java @@ -0,0 +1,181 @@ +package com.dlsc.gemsfx; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.TextInputControl; +import javafx.scene.layout.Region; +import javafx.util.Callback; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.prefs.Preferences; + +public interface SearchHistorySupport { + + int DEFAULT_MAX_HISTORY_SIZE = 30; + + boolean ENABLE_HISTORY_POPUP = true; + + boolean DEFAULT_ADDING_ITEM_TO_HISTORY_ON_ENTER = true; + + boolean DEFAULT_ADDING_ITEM_TO_HISTORY_ON_FOCUS_LOST = true; + + Region getNode(); + + /** + * Returns the text input control that is associated with this history support. + * + * @return the text input control + */ + TextInputControl getTextInputControl(); + + /** + * 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 + */ + void setHistory(List history); + + /** + * Adds the given item to the history. The method ensures that duplicates will not be added. + * + * @param item the item to add + */ + void addHistory(String item); + + /** + * Adds the given items to the history. + * + * @param items the items to add + */ + void addHistory(List items); + + /** + * Removes the given item from the history. + * + * @param item the item to remove + * @return true if the item was removed, false otherwise + */ + boolean removeHistory(String item); + + /** + * Removes the given items from the history. + * + * @param items the items to remove + */ + void removeHistory(List items); + + /** + * Clears the history. + */ + void clearHistory(); + + /** + * Returns an unmodifiable list of the history. + */ + ObservableList getUnmodifiableHistory(); + + /** + * Returns the property representing the maximum history size of the search text field. + * + * @return the maximum history size property + */ + IntegerProperty maxHistorySizeProperty(); + + int getMaxHistorySize(); + + void setMaxHistorySize(int maxHistorySize); + + /** + * Returns the property representing the history placeholder node. + * + * @return the property representing the history placeholder node + */ + ObjectProperty historyPlaceholderProperty(); + + Node getHistoryPlaceholder(); + + void setHistoryPlaceholder(Node historyPlaceholder); + + /** + * The cell factory for the history popup list view. + * + * @return the cell factory + */ + ObjectProperty, ListCell>> historyCellFactoryProperty(); + + void setHistoryCellFactory(Callback, ListCell> historyCellFactory); + + Callback, ListCell> getHistoryCellFactory(); + + /** + * Indicates whether the history popup should be enabled. + * + * @return true if the history popup should be enabled, false otherwise + */ + BooleanProperty enableHistoryPopupProperty(); + + boolean isEnableHistoryPopup(); + + void setEnableHistoryPopup(boolean enableHistoryPopup); + + /** + * Determines whether the text of the text field should be added to the history when the user presses the Enter key. + * + * @return true if the text should be added to the history on Enter, false otherwise + */ + BooleanProperty addingItemToHistoryOnEnterProperty(); + + boolean isAddingItemToHistoryOnEnter(); + + void setAddingItemToHistoryOnEnter(boolean addingItemToHistoryOnEnter); + + /** + * Determines whether the text of the text field should be added to the history when the field losses its focus. + * + * @return true if the text should be added to the history on focus lost, false otherwise + */ + BooleanProperty addingItemToHistoryOnFocusLostProperty(); + + boolean isAddingItemToHistoryOnFocusLost(); + + void setAddingItemToHistoryOnFocusLost(boolean addingItemToHistoryOnFocusLost); + + /** + * Indicates whether the history popup is showing. This is a read-only property. + * + * @return true if the history popup is showing, false otherwise + */ + ReadOnlyBooleanProperty historyPopupShowingProperty(); + + boolean isHistoryPopupShowing(); + + /** + * Stores a preferences object that will be used for persisting the search history of the field. + * + * @return the preferences used for persisting the search history + */ + ObjectProperty preferencesProperty(); + + Preferences getPreferences(); + + void setPreferences(Preferences preferences); + + /** + * 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 + */ + default List convertToUniqueList(List history) { + return history.stream().distinct().filter(StringUtils::isNotEmpty).limit(Math.max(0, getMaxHistorySize())).toList(); + } + +} diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/SearchTextField.java b/gemsfx/src/main/java/com/dlsc/gemsfx/SearchTextField.java index c1cbb7f0..e3524881 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/SearchTextField.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/SearchTextField.java @@ -1,6 +1,6 @@ package com.dlsc.gemsfx; -import com.dlsc.gemsfx.skins.SearchTextFieldHistoryPopup; +import com.dlsc.gemsfx.skins.SearchHistoryPopup; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.property.BooleanProperty; @@ -21,6 +21,7 @@ import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; @@ -50,20 +51,16 @@ * from a ListView or TableView that displays results, or through other interactions, by calling the {@link #addHistory} * method to add the current text to the history. */ -public class SearchTextField extends CustomTextField { +public class SearchTextField extends CustomTextField implements SearchHistorySupport { 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_ADDING_ITEM_TO_HISTORY_ON_ENTER = true; - private static final boolean DEFAULT_ADDING_ITEM_TO_HISTORY_ON_FOCUS_LOST = 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 SearchTextFieldHistoryPopup historyPopup; + private final boolean round; private final StackPane searchIconWrapper; + private SearchHistoryPopup searchHistoryPopup; /** * Constructs a new text field customized for search operations. @@ -82,6 +79,7 @@ public SearchTextField(boolean round) { if (round) { getStyleClass().add("round"); } + this.round = round; getStyleClass().add("search-text-field"); @@ -219,23 +217,26 @@ private void clickIconWrapperHandler(MouseEvent event) { return; } - if (historyPopup == null) { - historyPopup = new SearchTextFieldHistoryPopup(this); - historyPopupShowing.bind(historyPopup.showingProperty()); + if (searchHistoryPopup == null) { + searchHistoryPopup = new SearchHistoryPopup(this); + if (round) { + searchHistoryPopup.getStyleClass().add("round"); + } + historyPopupShowing.bind(searchHistoryPopup.showingProperty()); } - if (historyPopup.isShowing()) { - historyPopup.hide(); + if (searchHistoryPopup.isShowing()) { + searchHistoryPopup.hide(); } else { - historyPopup.show(this); + searchHistoryPopup.show(this); } positionCaret(textProperty().getValueSafe().length()); } private void hideHistoryPopup() { - if (historyPopup != null && historyPopup.isShowing()) { - historyPopup.hide(); + if (searchHistoryPopup != null && searchHistoryPopup.isShowing()) { + searchHistoryPopup.hide(); } } @@ -500,13 +501,14 @@ public final void setPreferences(Preferences preferences) { this.preferences.set(preferences); } - /** - * 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 convertToUniqueList(List history) { - return history.stream().distinct().filter(StringUtils::isNotEmpty).limit(Math.max(0, getMaxHistorySize())).toList(); + @Override + public final TextInputControl getTextInputControl() { + return this; } + + @Override + public Region getNode() { + return this; + } + } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchHistoryPopup.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchHistoryPopup.java new file mode 100644 index 00000000..67f8fa68 --- /dev/null +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchHistoryPopup.java @@ -0,0 +1,35 @@ +package com.dlsc.gemsfx.skins; + +import com.dlsc.gemsfx.CustomPopupControl; +import com.dlsc.gemsfx.SearchHistorySupport; +import javafx.scene.control.Skin; + +import java.util.Objects; + +public class SearchHistoryPopup extends CustomPopupControl { + + public static final String DEFAULT_STYLE_CLASS = "search-history-popup"; + + private final SearchHistorySupport historySupport; + + public SearchHistoryPopup(SearchHistorySupport historySupport) { + this.historySupport = Objects.requireNonNull(historySupport); + + getStyleClass().add(DEFAULT_STYLE_CLASS); + + maxWidthProperty().bind(this.historySupport.getNode().widthProperty()); + + setAutoFix(true); + setAutoHide(true); + setHideOnEscape(true); + } + + protected Skin createDefaultSkin() { + return new SearchHistoryPopupSkin(this); + } + + public final SearchHistorySupport getHistorySupport() { + return historySupport; + } + +} diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchTextFieldHistoryPopupSkin.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchHistoryPopupSkin.java similarity index 62% rename from gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchTextFieldHistoryPopupSkin.java rename to gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchHistoryPopupSkin.java index 65620fb3..4332f638 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchTextFieldHistoryPopupSkin.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchHistoryPopupSkin.java @@ -1,24 +1,25 @@ package com.dlsc.gemsfx.skins; +import com.dlsc.gemsfx.SearchHistorySupport; import com.dlsc.gemsfx.SearchField; -import com.dlsc.gemsfx.SearchTextField; import javafx.beans.binding.Bindings; import javafx.scene.Node; import javafx.scene.control.ListView; import javafx.scene.control.Skin; +import javafx.scene.control.TextInputControl; import javafx.scene.input.MouseButton; import java.util.Objects; -public class SearchTextFieldHistoryPopupSkin implements Skin { +public class SearchHistoryPopupSkin implements Skin { - private final SearchTextFieldHistoryPopup control; - private final SearchTextField searchTextField; + private final SearchHistoryPopup control; + private final SearchHistorySupport historySupport; private ListView listView; - public SearchTextFieldHistoryPopupSkin(SearchTextFieldHistoryPopup control) { + public SearchHistoryPopupSkin(SearchHistoryPopup control) { this.control = control; - searchTextField = control.getSearchTextField(); + historySupport = control.getHistorySupport(); initListView(); } @@ -27,15 +28,15 @@ private void initListView() { listView = new ListView<>() { @Override public String getUserAgentStylesheet() { - return Objects.requireNonNull(SearchField.class.getResource("search-text-field.css")).toExternalForm(); + return Objects.requireNonNull(SearchField.class.getResource("search-history-popup.css")).toExternalForm(); } }; listView.getStyleClass().add("search-history-list-view"); - Bindings.bindContent(listView.getItems(), searchTextField.getUnmodifiableHistory()); + Bindings.bindContent(listView.getItems(), historySupport.getUnmodifiableHistory()); - listView.cellFactoryProperty().bind(searchTextField.historyCellFactoryProperty()); - listView.placeholderProperty().bind(searchTextField.historyPlaceholderProperty()); + listView.cellFactoryProperty().bind(historySupport.historyCellFactoryProperty()); + listView.placeholderProperty().bind(historySupport.historyPlaceholderProperty()); listView.setOnMouseClicked(mouseEvent -> { if (mouseEvent.getButton() == MouseButton.PRIMARY && mouseEvent.getClickCount() == 1) { @@ -55,8 +56,9 @@ private void selectHistoryItem() { String selectedHistory = listView.getSelectionModel().getSelectedItem(); if (selectedHistory != null) { // replace text - int oldTextLen = control.getSearchTextField().textProperty().getValueSafe().length(); - searchTextField.replaceText(0, oldTextLen, selectedHistory); + TextInputControl textInputControl = historySupport.getTextInputControl(); + int oldTextLen = textInputControl.textProperty().getValueSafe().length(); + textInputControl.replaceText(0, oldTextLen, selectedHistory); // hide popup control.hide(); @@ -67,12 +69,12 @@ public Node getNode() { return listView; } - public SearchTextFieldHistoryPopup getSkinnable() { + public SearchHistoryPopup getSkinnable() { return control; } public void dispose() { - Bindings.unbindContent(listView.getItems(), searchTextField.getUnmodifiableHistory()); + Bindings.unbindContent(listView.getItems(), historySupport.getUnmodifiableHistory()); listView.prefWidthProperty().unbind(); listView.maxWidthProperty().unbind(); diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchTextFieldHistoryPopup.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchTextFieldHistoryPopup.java deleted file mode 100644 index 70ddb62e..00000000 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SearchTextFieldHistoryPopup.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.dlsc.gemsfx.skins; - -import com.dlsc.gemsfx.CustomPopupControl; -import com.dlsc.gemsfx.SearchTextField; -import javafx.scene.control.Skin; - -import java.util.Objects; - -public class SearchTextFieldHistoryPopup extends CustomPopupControl { - - public static final String DEFAULT_STYLE_CLASS = "search-text-field-history-popup"; - - private final SearchTextField searchTextField; - - public SearchTextFieldHistoryPopup(SearchTextField searchTextField) { - this.searchTextField = Objects.requireNonNull(searchTextField); - - getStyleClass().add(DEFAULT_STYLE_CLASS); - - maxWidthProperty().bind(searchTextField.widthProperty()); - - setAutoFix(true); - setAutoHide(true); - setHideOnEscape(true); - } - - protected Skin createDefaultSkin() { - return new SearchTextFieldHistoryPopupSkin(this); - } - - public SearchTextField getSearchTextField() { - return searchTextField; - } - -} diff --git a/gemsfx/src/main/resources/com/dlsc/gemsfx/search-history-popup.css b/gemsfx/src/main/resources/com/dlsc/gemsfx/search-history-popup.css new file mode 100644 index 00000000..eda2f5c0 --- /dev/null +++ b/gemsfx/src/main/resources/com/dlsc/gemsfx/search-history-popup.css @@ -0,0 +1,65 @@ +/* ----------------------------------------------------------------------- + * Style based on Modena.css combo-box-popup style + */ +.search-history-list-view { + -fx-background-color: linear-gradient(to bottom, + derive(-fx-color, -17%), + derive(-fx-color, -30%) + ), + -fx-control-inner-background; + -fx-background-insets: 0, 1; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 12, 0.0, 0, 8); + -fx-pref-height: 200px; +} + +.search-history-popup.round .search-history-list-view { + -fx-background-radius: 10px; + -fx-padding: 5px; +} + +.search-history-list-view .default-placeholder { + -fx-padding: 0 10px; +} + +.search-text-field-history-popup:above .search-history-list-view { + -fx-translate-y: -6; +} + +.search-history-list-view > .virtual-flow > .clipped-container > .sheet > .list-cell { + -fx-padding: 4 0 4 5; + /* No alternate highlighting */ + -fx-background: -fx-control-inner-background; +} + +.search-history-list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:hover { + -fx-background: -fx-accent; + -fx-background-color: #c9c9c9; +} + +.search-history-list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected, +.search-history-list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover { + -fx-background: -fx-accent; + -fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background; + -fx-background-insets: 0, 1, 2; +} + +.search-history-list-view > .placeholder > .label { + -fx-text-fill: derive(-fx-control-inner-background, -30%); +} + +.search-history-list-view .search-field-list-cell { +} + +.search-history-list-view .search-field-list-cell .text { + -fx-fill: -fx-selection-bar-text; +} + +.search-history-list-view .search-field-list-cell .text.start { +} + +.search-history-list-view .search-field-list-cell .text.middle { + -fx-underline: true; +} + +.search-history-list-view .search-field-list-cell .text.end { +} diff --git a/gemsfx/src/main/resources/com/dlsc/gemsfx/search-text-field.css b/gemsfx/src/main/resources/com/dlsc/gemsfx/search-text-field.css index 0e4f0173..c2b446ba 100644 --- a/gemsfx/src/main/resources/com/dlsc/gemsfx/search-text-field.css +++ b/gemsfx/src/main/resources/com/dlsc/gemsfx/search-text-field.css @@ -53,68 +53,7 @@ -fx-icon-color: -fx-text-inner-color; } -/* ----------------------------------------------------------------------- - * Style based on Modena.css combo-box-popup style - */ .search-text-field.round .search-history-list-view { -fx-background-radius: 10px; -fx-padding: 5px; -} - -.search-history-list-view { - -fx-background-color: linear-gradient(to bottom, - derive(-fx-color, -17%), - derive(-fx-color, -30%) - ), - -fx-control-inner-background; - -fx-background-insets: 0, 1; - -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 12, 0.0, 0, 8); - -fx-pref-height: 200px; -} - -.search-history-list-view .default-placeholder { - -fx-padding: 0 10px; -} - -.search-text-field-history-popup:above .search-history-list-view { - -fx-translate-y: -6; -} - -.search-history-list-view > .virtual-flow > .clipped-container > .sheet > .list-cell { - -fx-padding: 4 0 4 5; - /* No alternate highlighting */ - -fx-background: -fx-control-inner-background; -} - -.search-history-list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:hover { - -fx-background: -fx-accent; - -fx-background-color: #c9c9c9; -} - -.search-history-list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected, -.search-history-list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover { - -fx-background: -fx-accent; - -fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background; - -fx-background-insets: 0, 1, 2; -} - -.search-history-list-view > .placeholder > .label { - -fx-text-fill: derive(-fx-control-inner-background, -30%); -} - -.search-history-list-view .search-field-list-cell { -} - -.search-history-list-view .search-field-list-cell .text { - -fx-fill: -fx-selection-bar-text; -} - -.search-history-list-view .search-field-list-cell .text.start { -} - -.search-history-list-view .search-field-list-cell .text.middle { - -fx-underline: true; -} - -.search-history-list-view .search-field-list-cell .text.end { -} +} \ No newline at end of file