package org.limewire.ui.swing.filter; import java.awt.Color; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Comparator; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.DefaultListCellRenderer; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.ListSelectionModel; import javax.swing.border.Border; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import net.miginfocom.swing.MigLayout; import org.jdesktop.swingx.JXList; import org.jdesktop.swingx.decorator.ColorHighlighter; import org.jdesktop.swingx.decorator.HighlightPredicate; import org.limewire.collection.glazedlists.GlazedListsFactory; import org.limewire.core.api.FilePropertyKey; import org.limewire.ui.swing.components.HyperlinkButton; import org.limewire.ui.swing.components.RolloverCursorListener; import org.limewire.ui.swing.util.I18n; import org.limewire.ui.swing.util.IconManager; import org.limewire.util.Objects; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.FilterList; import ca.odell.glazedlists.UniqueList; import ca.odell.glazedlists.FunctionList.Function; import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEventListener; import ca.odell.glazedlists.matchers.Matcher; import ca.odell.glazedlists.swing.DefaultEventListModel; import ca.odell.glazedlists.swing.DefaultEventSelectionModel; import com.google.inject.Provider; /** * Filter component to select items according to a collection of property * values. */ class PropertyFilter<E extends FilterableItem> extends AbstractFilter<E> { private final FilterType filterType; private final FilePropertyKey propertyKey; private final Provider<IconManager> iconManager; private final JPanel panel = new JPanel(); private final JLabel propertyLabel = new JLabel(); private final JXList list = new JXList(); private final HyperlinkButton moreButton = new HyperlinkButton(); private EventList<Object> propertyList; private FilterList<Object> nonNullList; private UniqueListFactory<Object> uniqueListFactory; private UniqueList<Object> uniqueList; private DefaultEventSelectionModel<Object> selectionModel; private DefaultEventSelectionModel<Object> popupSelectionModel; private FilterPopupPanel morePopupPanel; /** * Constructs a PropertyFilter using the specified results list, * filter type, property key, and icon manager. */ public PropertyFilter(EventList<E> resultsList, FilterType filterType, FilePropertyKey propertyKey, Provider<IconManager> iconManager) { if ((filterType == FilterType.PROPERTY) && (propertyKey == null)) { throw new IllegalArgumentException("Property filter cannot use null key"); } this.filterType = filterType; this.propertyKey = propertyKey; this.iconManager = iconManager; FilterResources resources = getResources(); panel.setLayout(new MigLayout("insets 0 0 0 0, gap 0!, hidemode 3", "[left,grow]", "")); panel.setOpaque(false); propertyLabel.setFont(resources.getHeaderFont()); propertyLabel.setForeground(resources.getHeaderColor()); propertyLabel.setText(getPropertyText()); list.setCellRenderer(new PropertyCellRenderer(resources.getBackground(), BorderFactory.createEmptyBorder(1, 7, 0, 7))); list.setFont(resources.getRowFont()); list.setForeground(resources.getRowColor()); list.setOpaque(false); list.setRolloverEnabled(true); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // Add highlighter for rollover. list.setHighlighters(new ColorHighlighter(HighlightPredicate.ROLLOVER_ROW, resources.getHighlightBackground(), resources.getHighlightForeground())); // Add listener to show cursor on mouse over. new RolloverCursorListener().install(list); moreButton.setAction(new MoreAction()); moreButton.setBorder(BorderFactory.createEmptyBorder(0, 1, 1, 1)); moreButton.setContentAreaFilled(false); moreButton.setFocusPainted(false); moreButton.setFont(resources.getRowFont()); moreButton.setHorizontalTextPosition(JButton.LEADING); // Add listener to set popup trigger indicator. This activates logic // so that pressing "more" a second time closes an open popup. moreButton.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { if (morePopupPanel != null) { morePopupPanel.setPopupTriggered(true); } } @Override public void mouseExited(MouseEvent e) { if (morePopupPanel != null) { morePopupPanel.setPopupTriggered(false); } } }); // Apply results list to filter. initialize(resultsList); // Calculate max list height. list.setPrototypeCellValue("Type"); int listHeight = 3 * list.getFixedCellHeight(); panel.add(propertyLabel, "gap 6 6, wrap"); panel.add(list , "hmax " + listHeight + ", grow, wrap"); panel.add(moreButton , "gap 6 6"); } /** * Initializes the filter using the specified list of items. */ private void initialize(EventList<E> resultsList) { // Create list of unique property values. propertyList = createPropertyList(resultsList); nonNullList = createNonNullList(propertyList); uniqueListFactory = new UniqueListFactory<Object>(nonNullList, new PropertyComparator(filterType, propertyKey)); uniqueListFactory.setName(getPropertyText()); uniqueList = uniqueListFactory.getUniqueList(); // Initialize label and button visibility. propertyLabel.setVisible(uniqueList.size() > 0); moreButton.setVisible(uniqueList.size() > 3); // Add listener to display label and button when needed. uniqueList.addListEventListener(new ListEventListener<Object>() { @Override public void listChanged(ListEvent listChanges) { // Update label visibility. if (!propertyLabel.isVisible() && (uniqueList.size() > 0)) { propertyLabel.setVisible(true); } else if (propertyLabel.isVisible() && (uniqueList.size() < 1)) { propertyLabel.setVisible(false); } // Update "more" button visibility. if (!moreButton.isVisible() && (uniqueList.size() > 3)) { moreButton.setVisible(true); } else if (moreButton.isVisible() && (uniqueList.size() < 4)) { moreButton.setVisible(false); } } }); // Create sorted list to display most popular values. EventList<Object> sortedList = GlazedListsFactory.sortedList(uniqueList, new PropertyCountComparator()); // Create list and selection models. DefaultEventListModel<Object> listModel = new DefaultEventListModel<Object>(sortedList); selectionModel = new DefaultEventSelectionModel<Object>(sortedList); list.setModel(listModel); list.setSelectionModel(selectionModel); // Add selection listener to update filter. selectionModel.addListSelectionListener(new SelectionListener(selectionModel)); } @Override public JComponent getComponent() { return panel; } @Override public void reset() { // Clear selections. if (selectionModel != null) { selectionModel.clearSelection(); } if (popupSelectionModel != null) { popupSelectionModel.clearSelection(); } // Deactivate filter. deactivate(); } @Override public void dispose() { // Dispose of property list. uniqueListFactory.dispose(); propertyList.dispose(); } /** * Activates the filter using the specified text description and matcher. * This method also hides the filter component. */ @Override protected void activate(String activeText, Matcher<E> matcher) { super.activate(activeText, matcher); getComponent().setVisible(false); } /** * Deactivates the filter by clearing the text description and matcher. * This method also displays the filter component. */ @Override protected void deactivate() { super.deactivate(); getComponent().setVisible(true); } /** * Returns a text description of the filter state. */ @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getClass().getSimpleName()).append("["); buf.append("type=").append(filterType); buf.append(", property=").append(propertyKey); buf.append(", uniqueItems=").append(uniqueList.size()); buf.append(", active=").append(isActive()); EventList<Object> selectedList = selectionModel.getSelected(); buf.append(", selection=").append((selectedList.size() > 0) ? selectedList.get(0) : "null"); buf.append("]"); return buf.toString(); } /** * Returns the text string describing the property for the current filter * type and property key. */ private String getPropertyText() { switch (filterType) { case EXTENSION: return I18n.tr("Extensions"); case PROPERTY: switch (propertyKey) { case AUTHOR: return I18n.tr("Artists"); case ALBUM: return I18n.tr("Albums"); case GENRE: return I18n.tr("Genres"); default: return propertyKey.toString(); } case FILE_TYPE: return I18n.tr("Types"); default: throw new IllegalStateException("Unknown filter type " + filterType); } } /** * Returns a list of property values in the specified list of items. */ private EventList<Object> createPropertyList(EventList<E> resultsList) { switch (filterType) { case EXTENSION: case PROPERTY: case FILE_TYPE: // Create list of property values. return GlazedListsFactory.simpleFunctionList(resultsList, new PropertyFunction(filterType, propertyKey)); default: throw new IllegalArgumentException("Invalid filter type " + filterType); } } /** * Returns a list of non-null values in the specified list of property * values. */ private FilterList<Object> createNonNullList(EventList<Object> propertyList) { // Create list of non-null values. return GlazedListsFactory.filterList(propertyList, new Matcher<Object>() { @Override public boolean matches(Object item) { return (item != null); } } ); } /** * Creates a popup to display the complete list of property values. */ private FilterPopupPanel createMorePopup() { FilterPopupPanel popupPanel = new FilterPopupPanel(getResources(), getPropertyText()); // Set list cell renderer. popupPanel.setListCellRenderer(new PropertyCellRenderer(popupPanel.getBackground(), BorderFactory.createEmptyBorder(1, 4, 0, 1))); // Set list and selection models. We use the unique list directly // to display values alphabetically. DefaultEventListModel<Object> listModel = new DefaultEventListModel<Object>(uniqueList); popupSelectionModel = new DefaultEventSelectionModel<Object>(uniqueList); popupPanel.setListModel(listModel); popupPanel.setListSelectionModel(popupSelectionModel); // Add selection listener to update filter. popupSelectionModel.addListSelectionListener(new SelectionListener(popupSelectionModel)); return popupPanel; } /** * Displays the "more" popup that lists all property values. */ private void showMorePopup() { if (morePopupPanel == null) { morePopupPanel = createMorePopup(); } morePopupPanel.showPopup(moreButton, list.getWidth() - 12, propertyLabel.getY() - moreButton.getY()); } /** * Hides the "more" popup that lists all property values. */ private void hideMorePopup() { if (morePopupPanel != null) { morePopupPanel.hidePopup(); } } /** * Action to display list of all property values in popup window. */ private class MoreAction extends AbstractAction { public MoreAction() { super(I18n.tr("more")); } @Override public void actionPerformed(ActionEvent e) { if (morePopupPanel == null) { showMorePopup(); } else if (morePopupPanel.isPopupReady()) { showMorePopup(); } else { morePopupPanel.setPopupReady(true); } } } /** * Listener to handle selection changes to update the matcher editor. */ private class SelectionListener implements ListSelectionListener { private final DefaultEventSelectionModel<Object> selectionModel; public SelectionListener(DefaultEventSelectionModel<Object> selectionModel) { this.selectionModel = selectionModel; } @Override public void valueChanged(ListSelectionEvent e) { // Skip selection change if filter is active. if (isActive()) { return; } // Get list of selected values. EventList<Object> selectedList = selectionModel.getSelected(); if (selectedList.size() > 0) { // Create new matcher and activate. Matcher<E> newMatcher = new PropertyMatcher<E>(filterType, propertyKey, iconManager, selectedList); activate(selectedList.get(0).toString(), newMatcher); } else { // Deactivate to clear matcher. deactivate(); } // Hide popup if showing. hideMorePopup(); // Notify filter listeners. fireFilterChanged(PropertyFilter.this); } } /** * Cell renderer for property values. */ private class PropertyCellRenderer extends DefaultListCellRenderer { private final Color background; private final Border border; public PropertyCellRenderer(Color background, Border border) { this.background = background; this.border = border; } @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Component renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if ((renderer instanceof JLabel) && (value != null)) { // Get count for property value. int count = uniqueList.getCount(value); // Display count in cell. StringBuilder buf = new StringBuilder(); buf.append(value.toString()).append(" (").append(count).append(")"); ((JLabel) renderer).setText(buf.toString()); // Set appearance. ((JLabel) renderer).setBackground(background); ((JLabel) renderer).setBorder(border); } return renderer; } } /** * Comparator to sort property values by their result count. */ private class PropertyCountComparator implements Comparator<Object> { @Override public int compare(Object o1, Object o2) { int count1 = uniqueList.getCount(o1); int count2 = uniqueList.getCount(o2); // Return inverse value to sort in descending order. return (count1 < count2) ? 1 : ((count1 > count2) ? -1 : 0); } } /** * A Comparator for values in a specific property. This is used to create * a list of unique property values. */ private static class PropertyComparator implements Comparator<Object> { private final FilterType filterType; private final FilePropertyKey propertyKey; public PropertyComparator(FilterType filterType, FilePropertyKey propertyKey) { this.filterType = filterType; this.propertyKey = propertyKey; } @Override public int compare(Object o1, Object o2) { if ((filterType == FilterType.PROPERTY) && FilePropertyKey.isLong(propertyKey)) { Long long1 = (Long) o1; Long long2 = (Long) o2; return Objects.compareToNull(long1, long2, false); } else { String s1 = (String) o1; String s2 = (String) o2; return Objects.compareToNullIgnoreCase(s1, s2, false); } } } /** * A function to transform a list of filterable items into a list of * specific property values. */ private class PropertyFunction implements Function<E, Object> { private final FilterType filterType; private final FilePropertyKey propertyKey; public PropertyFunction(FilterType filterType, FilePropertyKey propertyKey) { this.filterType = filterType; this.propertyKey = propertyKey; } @Override public Object evaluate(E item) { switch (filterType) { case EXTENSION: return item.getFileExtension().toLowerCase(); case PROPERTY: return item.getProperty(propertyKey); case FILE_TYPE: return iconManager.get().getMIMEDescription(item.getFileExtension()); default: return null; } } } }