/** * Copyright (C) 2019 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.unitConverter.converterGUI; import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.function.Predicate; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; /** * @author Adrien Hopkins * @since 2019-04-13 * @since v0.2.0 */ final class SearchBoxList extends JPanel { /** * @since 2019-04-13 * @since v0.2.0 */ private static final long serialVersionUID = 6226930279415983433L; /** * The text to place in an empty search box. * * @since 2019-04-13 * @since v0.2.0 */ private static final String EMPTY_TEXT = "Search..."; /** * The color to use for an empty foreground. * * @since 2019-04-13 * @since v0.2.0 */ private static final Color EMPTY_FOREGROUND = new Color(192, 192, 192); // the components private final Collection itemsToFilter; private final DelegateListModel listModel; private final JTextField searchBox; private final JList searchItems; private boolean searchBoxEmpty = true; // I need to do this because, for some reason, Swing is auto-focusing my search box without triggering a focus // event. private boolean searchBoxFocused = false; private Predicate customSearchFilter = o -> true; private final Comparator defaultOrdering; private final boolean caseSensitive; /** * Creates the {@code SearchBoxList}. * * @param itemsToFilter * items to put in the list * @since 2019-04-14 */ public SearchBoxList(final Collection itemsToFilter) { this(itemsToFilter, null, false); } /** * Creates the {@code SearchBoxList}. * * @param itemsToFilter * items to put in the list * @param defaultOrdering * default ordering of items after filtration (null=Comparable) * @param caseSensitive * whether or not the filtration is case-sensitive * * @since 2019-04-13 * @since v0.2.0 */ public SearchBoxList(final Collection itemsToFilter, final Comparator defaultOrdering, final boolean caseSensitive) { super(new BorderLayout(), true); this.itemsToFilter = itemsToFilter; this.defaultOrdering = defaultOrdering; this.caseSensitive = caseSensitive; // create the components this.listModel = new DelegateListModel<>(new ArrayList<>(itemsToFilter)); this.searchItems = new JList<>(this.listModel); this.searchBox = new JTextField(EMPTY_TEXT); this.searchBox.setForeground(EMPTY_FOREGROUND); // add them to the panel this.add(this.searchBox, BorderLayout.PAGE_START); this.add(new JScrollPane(this.searchItems), BorderLayout.CENTER); // set up the search box this.searchBox.addFocusListener(new FocusListener() { @Override public void focusGained(final FocusEvent e) { SearchBoxList.this.searchBoxFocusGained(e); } @Override public void focusLost(final FocusEvent e) { SearchBoxList.this.searchBoxFocusLost(e); } }); this.searchBox.addCaretListener(e -> this.searchBoxTextChanged()); this.searchBoxEmpty = true; } /** * Adds an additional filter for searching. * * @param filter * filter to add. * @since 2019-04-13 * @since v0.2.0 */ public void addSearchFilter(final Predicate filter) { this.customSearchFilter = this.customSearchFilter.and(filter); } /** * Resets the search filter. * * @since 2019-04-13 * @since v0.2.0 */ public void clearSearchFilters() { this.customSearchFilter = o -> true; } /** * @return this component's search box component * @since 2019-04-14 * @since v0.2.0 */ public final JTextField getSearchBox() { return this.searchBox; } /** * @param searchText * text to search for * @return a filter that filters out that text, based on this list's case sensitive setting * @since 2019-04-14 * @since v0.2.0 */ private Predicate getSearchFilter(final String searchText) { if (this.caseSensitive) return string -> string.contains(searchText); else return string -> string.toLowerCase().contains(searchText.toLowerCase()); } /** * @return this component's list component * @since 2019-04-14 * @since v0.2.0 */ public final JList getSearchList() { return this.searchItems; } /** * @return index selected in item list * @since 2019-04-14 * @since v0.2.0 */ public int getSelectedIndex() { return this.searchItems.getSelectedIndex(); } /** * @return value selected in item list * @since 2019-04-13 * @since v0.2.0 */ public String getSelectedValue() { return this.searchItems.getSelectedValue(); } /** * Re-applies the filters. * * @since 2019-04-13 * @since v0.2.0 */ public void reapplyFilter() { final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive); final Predicate searchFilter = this.getSearchFilter(searchText); this.listModel.clear(); this.itemsToFilter.forEach(string -> { if (searchFilter.test(string)) { this.listModel.add(string); } }); // applies the custom filters this.listModel.removeIf(this.customSearchFilter.negate()); // sorts the remaining items this.listModel.sort(comparator); } /** * Runs whenever the search box gains focus. * * @param e * focus event * @since 2019-04-13 * @since v0.2.0 */ private void searchBoxFocusGained(final FocusEvent e) { this.searchBoxFocused = true; if (this.searchBoxEmpty) { this.searchBox.setText(""); this.searchBox.setForeground(Color.BLACK); } } /** * Runs whenever the search box loses focus. * * @param e * focus event * @since 2019-04-13 * @since v0.2.0 */ private void searchBoxFocusLost(final FocusEvent e) { this.searchBoxFocused = false; if (this.searchBoxEmpty) { this.searchBox.setText(EMPTY_TEXT); this.searchBox.setForeground(EMPTY_FOREGROUND); } } /** * Runs whenever the text in the search box is changed. *

* Reapplies the search filter, and custom filters. *

* * @since 2019-04-14 * @since v0.2.0 */ private void searchBoxTextChanged() { if (this.searchBoxFocused) { this.searchBoxEmpty = this.searchBox.getText().equals(""); } final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); final FilterComparator comparator = new FilterComparator(searchText, this.defaultOrdering, this.caseSensitive); final Predicate searchFilter = this.getSearchFilter(searchText); // initialize list with items that match the filter then sort this.listModel.clear(); this.itemsToFilter.forEach(string -> { if (searchFilter.test(string)) { this.listModel.add(string); } }); // applies the custom filters this.listModel.removeIf(this.customSearchFilter.negate()); // sorts the remaining items this.listModel.sort(comparator); } }