package org.limewire.ui.swing.filter; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import net.miginfocom.swing.MigLayout; import org.jdesktop.application.Resource; import org.limewire.core.api.Category; import org.limewire.core.api.search.SearchCategory; import org.limewire.core.api.search.SearchDetails.SearchType; import org.limewire.ui.swing.components.Disposable; import org.limewire.ui.swing.components.HyperlinkButton; import org.limewire.ui.swing.components.Line; import org.limewire.ui.swing.components.PromptTextField; import org.limewire.ui.swing.components.SideLineBorder; import org.limewire.ui.swing.components.SideLineBorder.Side; import org.limewire.ui.swing.components.decorators.TextFieldDecorator; import org.limewire.ui.swing.painter.BorderPainter.AccentType; import org.limewire.ui.swing.util.GuiUtils; import org.limewire.ui.swing.util.I18n; import org.limewire.ui.swing.util.IconManager; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.matchers.CompositeMatcherEditor; import ca.odell.glazedlists.matchers.MatcherEditor; import ca.odell.glazedlists.swing.TextComponentMatcherEditor; import com.google.inject.Provider; /** * Filter panel for filterable data. AdvancedFilterPanel presents advanced * filtering options, including an input text field and category-specific * property filters. */ public class AdvancedFilterPanel<E extends FilterableItem> extends JPanel implements Disposable { private static final int LEFT_INSET = 6; private static final int RIGHT_INSET = 6; @Resource(key="AdvancedFilter.filterWidth") private int filterWidth; @Resource(key="AdvancedFilter.background") private Color background; @Resource private Color borderColor; @Resource private Color dividerBackground; @Resource private Color dividerForeground; @Resource private Font moreTextFont; @Resource private Font resetTextFont; /** Filterable data source. */ private final FilterableSource<E> filterableSource; /** List of editors being used for filtering. */ private final EventList<MatcherEditor<E>> editorList; /** Manager for filters. */ private final FilterManager<E> filterManager; /** List of category selection listeners. */ private final List<CategoryListener> listenerList = new ArrayList<CategoryListener>(); /** Filter for file category. */ private final CategoryFilter<E> categoryFilter; /** Filter for file source. */ private final SourceFilter<E> sourceFilter; /** Text field for text filter. */ private final PromptTextField filterTextField = new PromptTextField(I18n.tr("Refine results...")); /** Container to display active filters. */ private final FilterDisplayPanel filterDisplayPanel; /** Container to display filter components. */ private final JPanel filterPanel = new JPanel(); private final Line upperDividerLine; private final Line lowerDividerLine; private final JScrollPane filterScrollPane = new JScrollPane(); /** Container for category-specific filters. */ private final PropertyFilterPanel propertyPanel; /** Default display category; determines the default table format. */ private SearchCategory defaultDisplayCategory; /** Category that determines the default filters to display. */ private SearchCategory defaultFilterCategory; /** Indicator that determines whether filter layout is being adjusted. */ private boolean layoutAdjusting; /** * Constructs a FilterPanel with the specified filterable data source * and UI decorators. */ public AdvancedFilterPanel(FilterableSource<E> filterableSource, TextFieldDecorator textFieldDecorator, Provider<IconManager> iconManager, SearchType type) { this.filterableSource = filterableSource; this.editorList = new BasicEventList<MatcherEditor<E>>(); this.filterManager = new FilterManager<E>(filterableSource, iconManager); if (filterableSource.getFilterDebugger() != null) { filterableSource.getFilterDebugger().initialize(filterManager); } GuiUtils.assignResources(this); setBackground(background); setBorder(new SideLineBorder(borderColor, Side.RIGHT)); setLayout(new MigLayout("insets 0 0 0 0, gap 0!, fill, hidemode 3")); int textFieldWidth = filterWidth - LEFT_INSET - RIGHT_INSET; textFieldDecorator.decorateClearablePromptField(filterTextField, AccentType.NONE); filterTextField.setMinimumSize(new Dimension(textFieldWidth, filterTextField.getMinimumSize().height)); filterTextField.setPreferredSize(new Dimension(textFieldWidth, filterTextField.getPreferredSize().height)); filterDisplayPanel = new FilterDisplayPanel(); filterPanel.setBackground(background); filterPanel.setLayout(new MigLayout("insets 0 0 0 0, gap 0!, hidemode 3", "[grow]", "")); upperDividerLine = Line.createHorizontalLine(dividerForeground); lowerDividerLine = Line.createHorizontalLine(dividerBackground); filterScrollPane.setBorder(BorderFactory.createEmptyBorder()); filterScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); filterScrollPane.setViewportView(filterPanel); filterScrollPane.getVerticalScrollBar().setUnitIncrement(5); // Add listener to update filter layout when scroll bar appears. filterScrollPane.getVerticalScrollBar().addComponentListener(new ComponentAdapter() { @Override public void componentHidden(ComponentEvent e) { if (!layoutAdjusting) doFilterLayout(); } @Override public void componentShown(ComponentEvent e) { if (!layoutAdjusting) doFilterLayout(); } }); propertyPanel = new PropertyFilterPanel(); // Create category filter and display component. categoryFilter = filterManager.getCategoryFilter(); JComponent categoryComp = categoryFilter.getComponent(); categoryComp.setVisible(filterableSource.getFilterCategory() == SearchCategory.ALL); // Create source filter and display component. sourceFilter = filterManager.getSourceFilter(); sourceFilter.getComponent().setVisible(false); if (type != SearchType.SINGLE_BROWSE) { // Add listener to show source filter when friend results are received. sourceFilter.addFriendListener(new SourceFilter.FriendListener() { @Override public void friendFound(boolean found) { if (!sourceFilter.isActive()) { sourceFilter.getComponent().setVisible(found); } } }); } // Layout components. add(filterTextField , "gap 6 6 6 6, growx, wrap"); add(filterDisplayPanel, "gap 0 0 2 0, growx, wrap"); add(upperDividerLine , "gap 0 0 6 0, hmin 1, growx, wrap"); add(lowerDividerLine , "gap 0 0 0 0, hmin 1, growx, wrap"); add(filterScrollPane , "gap 0 0 0 0, grow, push"); doFilterLayout(); configureFilters(); } /** * Updates the layout of the filter components depending on the state of * the vertical scroll bar. */ private void doFilterLayout() { layoutAdjusting = true; try { // Determine max width. JScrollBar scrollBar = filterScrollPane.getVerticalScrollBar(); int maxWidth = filterWidth - (scrollBar.isVisible() ? scrollBar.getWidth() : 0); // Add components using max width. filterPanel.removeAll(); filterPanel.add(categoryFilter.getComponent(), "gap 0 0 8 6, wmax " + maxWidth + ", growx, wrap"); filterPanel.add(sourceFilter.getComponent() , "gap 0 0 8 6, wmax " + maxWidth + ", growx, wrap"); filterPanel.add(propertyPanel , "gap 0 0 0 0, wmax " + maxWidth + ", grow"); // Update divider visibility. updateDivider(); validate(); repaint(); } finally { layoutAdjusting = false; } } /** * Updates the visibility of the divider. */ private void updateDivider() { boolean visible = filterDisplayPanel.isVisible() || filterScrollPane.getVerticalScrollBar().isVisible(); upperDividerLine.setVisible(visible); lowerDividerLine.setVisible(visible); } /** * Configures the filters by creating a composite filter that uses a list * of MatcherEditor objects. */ private void configureFilters() { // Create text filter with "live" filtering. MatcherEditor<E> editor = new TextComponentMatcherEditor<E>( filterTextField, new FilterableItemTextFilterator<E>(), true); // Add text filter to editor list. editorList.add(editor); // Create CompositeMatcherEditor to combine filters. CompositeMatcherEditor<E> compositeEditor = new CompositeMatcherEditor<E>(editorList); // Configure filter in data model. filterableSource.setFilterEditor(compositeEditor); // Hide filter display. filterDisplayPanel.setVisible(false); // Add listeners to standard filters. categoryFilter.addFilterListener(new AddFilterListener()); sourceFilter.addFilterListener(new AddFilterListener()); } /** * Adds the specified filter to the list of active filters. */ private void addActiveFilter(Filter<E> filter) { // Add matcher/editor to list. MatcherEditor<E> editor = filter.getMatcherEditor(); if ((editor != null) && !editorList.contains(editor)) { editorList.add(editor); } // Add filter to display. filterDisplayPanel.addFilter(filter); } /** * Removes the specified filter from the list of active filters. */ private void removeActiveFilter(Filter<E> filter) { // Remove filter from display. filterDisplayPanel.removeFilter(filter); // Remove matcher/editor from list. MatcherEditor<E> editor = filter.getMatcherEditor(); if ((editor != null) && editorList.contains(editor)) { editorList.remove(editor); } // Reset filter for use. filter.reset(); } /** * Updates the category. There are two distinct category settings: the * "filter category" determines the displayed property filters, while the * "display category" determines the table column layout. These may be * different. This method updates the filter category in * PropertyFilterPanel, and fires a <code>categorySelected</code> event to * update the display category. */ private void updateCategory() { if (categoryFilter.isActive()) { // Get selected category. Category category = categoryFilter.getSelectedCategory(); SearchCategory displayCategory = SearchCategory.forCategory(category); // Apply category to property filters and fire event. propertyPanel.setFilterCategory(displayCategory); fireCategorySelected(displayCategory); } else if (categoryFilter.getCategoryCount() == 1) { // Get only remaining category. Category category = categoryFilter.getDefaultCategory(); SearchCategory displayCategory = SearchCategory.forCategory(category); // Apply category to property filters and fire event. propertyPanel.setFilterCategory(displayCategory); fireCategorySelected(displayCategory); } else { // No specific category so reapply defaults. propertyPanel.setFilterCategory(defaultFilterCategory); fireCategorySelected(defaultDisplayCategory); } } /** * Adds the specified listener to the list that is notified when the * display category changes. */ public void addCategoryListener(CategoryListener listener) { listenerList.add(listener); } /** * Removes the specified listener from the list that is notified when the * display category changes. */ public void removeCategoryListener(CategoryListener listener) { listenerList.remove(listener); } /** * Notifies registered listeners when the specified category should be * displayed. */ private void fireCategorySelected(SearchCategory displayCategory) { for (int i = 0, size = listenerList.size(); i < size; i++) { listenerList.get(i).categorySelected(displayCategory); } } /** * Sets the default display category, and updates the available filters. */ public void setSearchCategory(SearchCategory searchCategory) { // Save default display and filter categories. defaultDisplayCategory = searchCategory; defaultFilterCategory = searchCategory; if (searchCategory == SearchCategory.ALL) { // Start detector to determine default filter category based on // actual list of filterable items. CategoryDetector detector = new CategoryDetector<E>(filterableSource, categoryFilter); detector.start(new CategoryDetector.CategoryDetectorListener() { @Override public void categoryFound(Category category) { defaultFilterCategory = (category != null) ? SearchCategory.forCategory(category) : SearchCategory.ALL; propertyPanel.setFilterCategory(defaultFilterCategory); } }); } else { // Update filters using default filter category. propertyPanel.setFilterCategory(defaultFilterCategory); } } @Override public void dispose() { filterManager.dispose(); } /** * Listener to apply a filter when its state changes. */ private class AddFilterListener implements FilterListener<E> { @Override public void filterChanged(Filter<E> filter) { if (filter.isActive()) { addActiveFilter(filter); } else { removeActiveFilter(filter); } updateCategory(); } } /** * Action to remove all active filters. */ private class RemoveAllAction extends AbstractAction { public RemoveAllAction() { putValue(Action.NAME, I18n.tr("reset")); } @Override public void actionPerformed(ActionEvent e) { // Clear text filter. filterTextField.setText(null); // Remove active filters. List<Filter<E>> filterList = filterDisplayPanel.getActiveFilters(); for (Filter<E> filter : filterList) { removeActiveFilter(filter); } // Update display category. updateCategory(); } } /** * Action to remove active filter. */ private class RemoveFilterAction extends AbstractAction { private final Filter<E> filter; public RemoveFilterAction(Filter<E> filter) { this.filter = filter; putValue(Action.NAME, filter.getActiveText()); } @Override public void actionPerformed(ActionEvent e) { removeActiveFilter(filter); updateCategory(); } } /** * Action to display more or less filters. */ private class MoreFilterAction extends AbstractAction { private final String MORE = I18n.tr("more filters"); private final String LESS = I18n.tr("fewer filters"); public MoreFilterAction() { putValue(Action.NAME, MORE); } @Override public void actionPerformed(ActionEvent e) { propertyPanel.setShowAll(!propertyPanel.isShowAll()); update(propertyPanel.isShowAll()); } public void update(boolean showAll) { putValue(Action.NAME, (showAll ? LESS : MORE)); } } /** * Panel that displays the active filters applied to the items. */ private class FilterDisplayPanel extends JPanel { private final JPanel displayPanel = new JPanel(); private final HyperlinkButton resetButton = new HyperlinkButton(); private final Map<Filter<E>, ActiveFilterPanel> displayMap = new HashMap<Filter<E>, ActiveFilterPanel>(); public FilterDisplayPanel() { setLayout(new MigLayout("insets 0 0 0 0, gap 0!, hidemode 3", "[grow]", "")); setOpaque(false); displayPanel.setLayout(new MigLayout("insets 0 0 0 0, gap 0!", "[grow]", "")); displayPanel.setOpaque(false); resetButton.setAction(new RemoveAllAction()); resetButton.setFont(resetTextFont); add(displayPanel, "gap 6 6 0 0, growx, wrap"); add(resetButton , "gap 6 6 0 0, alignx right, wrap"); } /** * Adds the specified filter to the display. */ public void addFilter(Filter<E> filter) { if (displayMap.get(filter) != null) { removeFilter(filter); } // Create filter display and save in map. ActiveFilterPanel activeFilterPanel = new ActiveFilterPanel(new RemoveFilterAction(filter)); displayMap.put(filter, activeFilterPanel); // Add filter display to container. int maxWidth = filterWidth - LEFT_INSET - RIGHT_INSET; displayPanel.add(activeFilterPanel, "gaptop 4, wmax " + maxWidth + ", wrap"); // Display this container. setVisible(true); updateDivider(); // Repaint filter display. AdvancedFilterPanel.this.validate(); AdvancedFilterPanel.this.repaint(); } /** * Removes the specified filter from the display. */ public void removeFilter(Filter<E> filter) { // Remove filter display from container. ActiveFilterPanel activeFilterPanel = displayMap.get(filter); if (activeFilterPanel != null) { displayPanel.remove(activeFilterPanel); } // Remove filter display from map. displayMap.remove(filter); // Hide this container if no active filters. if (displayMap.size() < 1) { setVisible(false); updateDivider(); } // Repaint filter display. AdvancedFilterPanel.this.validate(); AdvancedFilterPanel.this.repaint(); } /** * Returns a list of filters currently in use. */ public List<Filter<E>> getActiveFilters() { return new ArrayList<Filter<E>>(displayMap.keySet()); } } /** * Panel that displays property filters associated with the current filter * category. */ private class PropertyFilterPanel extends JPanel implements FilterListener<E> { private final HyperlinkButton moreButton = new HyperlinkButton(); /** Action to toggle "show-all" filters state. */ private final MoreFilterAction moreFilterAction = new MoreFilterAction(); /** Map of "show-all" indicators by search category. */ private final Map<SearchCategory, Boolean> showAllMap = new EnumMap<SearchCategory, Boolean>(SearchCategory.class); /** List of available filters. */ private List<Filter<E>> filterList = Collections.emptyList(); private SearchCategory currentCategory; public PropertyFilterPanel() { setLayout(new MigLayout("insets 0 0 0 0, gap 0!, hidemode 3", "[grow]", "")); setOpaque(false); moreButton.setAction(moreFilterAction); moreButton.setFont(moreTextFont); } /** * Sets the specified filter category, and updates the visible filters. */ public void setFilterCategory(SearchCategory filterCategory) { // Skip if category not changed. if (currentCategory == filterCategory) { return; } currentCategory = filterCategory; // Save old property filters. List<Filter<E>> oldFilterList = filterList; // Get new property filters for category. filterList = filterManager.getPropertyFilterList(filterCategory); int filterMin = filterManager.getPropertyFilterMinimum(filterCategory); // Remove old filters, and deactivate filters that are NOT in the // list of new filters. removeAll(); for (Filter<E> filter : oldFilterList) { filter.removeFilterListener(this); if (!filterList.contains(filter)) { removeActiveFilter(filter); } } // Add new filters to container, and set visibility for filters // that are not active. for (int i = 0, size = filterList.size(); i < size; i++) { Filter<E> filter = filterList.get(i); JComponent component = filter.getComponent(); add(component, "gap 0 0 8 6, aligny top, growx, wrap"); if (!filter.isActive()) { component.setVisible(isFilterVisible(i)); } filter.addFilterListener(this); } // Add more/less button if needed. if ((filterMin > 0) && (filterList.size() > filterMin)) { add(moreButton, "gap 6 6 8 3, aligny top"); } // Update more/less button. moreFilterAction.update(isShowAll()); // Validate layout and repaint container. validate(); repaint(); } /** * Sets an indicator to display all property filters for the current * category. If <code>showAll</code> is false, then only the minimum * number of filters is displayed. */ public void setShowAll(boolean showAll) { // Save indicator for category. showAllMap.put(currentCategory, showAll); // Set visibility for current filters. for (int i = 0, size = filterList.size(); i < size; i++) { Filter<E> filter = filterList.get(i); JComponent component = filter.getComponent(); if (!filter.isActive()) { component.setVisible(isFilterVisible(i)); } } } /** * Returns an indicator that determines whether all property filters * are displayed for the current category, or only the minimum number. */ public boolean isShowAll() { Boolean showAll = showAllMap.get(currentCategory); return (showAll != null) ? showAll.booleanValue() : false; } /** * Returns an indicator that determines whether the filter at the * specified index is visible. */ private boolean isFilterVisible(int index) { // Get showAll indicator for category. boolean visible = isShowAll(); // If not showAll, compare index with minimum filter count. if (!visible) { int filterMin = filterManager.getPropertyFilterMinimum(currentCategory); visible = (filterMin < 1) || (index < filterMin); } return visible; } @Override public void filterChanged(Filter<E> filter) { if (filter.isActive()) { addActiveFilter(filter); } else { removeActiveFilter(filter); } updateCategory(); } } /** * Defines a listener to handle display category events. */ public static interface CategoryListener { /** * Invoked when the specified display category is selected. */ void categorySelected(SearchCategory displayCategory); } }