/** * 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.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 */ final class SearchBoxList extends JPanel { /** * @since 2019-04-13 */ private static final long serialVersionUID = 6226930279415983433L; /** * The text to place in an empty search box. */ private static final String EMPTY_TEXT = "Search..."; /** * The color to use for an empty foreground. */ 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 searchFilter = o -> true; /** * Creates the {@code SearchBoxList}. * * @since 2019-04-13 */ public SearchBoxList(final Collection itemsToFilter) { super(new BorderLayout(), true); this.itemsToFilter = itemsToFilter; // 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 */ public void addSearchFilter(final Predicate filter) { this.searchFilter = this.searchFilter.and(filter); } /** * Resets the search filter. * * @since 2019-04-13 */ public void clearSearchFilters() { this.searchFilter = o -> true; } /** * @return value selected in item list * @since 2019-04-13 */ public String getSelectedValue() { return this.searchItems.getSelectedValue(); } /** * Re-applies the filters. * * @since 2019-04-13 */ public void reapplyFilter() { final String searchText = this.searchBoxEmpty ? "" : this.searchBox.getText(); final FilterComparator comparator = new FilterComparator(searchText); this.listModel.clear(); this.itemsToFilter.forEach(string -> { if (string.toLowerCase().contains(searchText.toLowerCase())) { this.listModel.add(string); } }); // applies the custom filters this.listModel.removeIf(this.searchFilter.negate()); // sorts the remaining items this.listModel.sort(comparator); } /** * Runs whenever the search box gains focus. * * @param e * focus event * @since 2019-04-13 */ 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 */ private void searchBoxFocusLost(final FocusEvent e) { this.searchBoxFocused = false; if (this.searchBoxEmpty) { this.searchBox.setText(EMPTY_TEXT); this.searchBox.setForeground(EMPTY_FOREGROUND); } } 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); // initialize list with items that match the filter then sort this.listModel.clear(); this.itemsToFilter.forEach(string -> { if (string.toLowerCase().contains(searchText.toLowerCase())) { this.listModel.add(string); } }); // applies the custom filters this.listModel.removeIf(this.searchFilter.negate()); // sorts the remaining items this.listModel.sort(comparator); } }