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.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; 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.SwingUtilities; 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.friend.api.Friend; import org.limewire.ui.swing.components.HyperlinkButton; import org.limewire.ui.swing.components.RolloverCursorListener; import org.limewire.ui.swing.util.I18n; import org.limewire.util.Objects; import ca.odell.glazedlists.CollectionList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.SortedList; import ca.odell.glazedlists.TransformedList; 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; /** * Filter component to select items according to their sources. */ class SourceFilter<E extends FilterableItem> extends AbstractFilter<E> { private final JPanel panel = new JPanel(); private final JLabel label = new JLabel(); private final JXList list = new JXList(); private final HyperlinkButton moreButton = new HyperlinkButton(); private final List<FriendListener> friendListenerList = new ArrayList<FriendListener>(); private EventList<SourceItem> sourceList; private UniqueListFactory<SourceItem> uniqueSourceListFactory; private UniqueList<SourceItem> uniqueSourceList; private EventList<SourceItem> friendList; private UniqueListFactory<SourceItem> uniqueFriendListFactory; private UniqueList<SourceItem> uniqueFriendList; private UniqueList<SourceItem> currentUniqueList; private SortedList<SourceItem> sortedList; private DefaultEventListModel<SourceItem> listModel; private DefaultEventSelectionModel<SourceItem> selectionModel; private DefaultEventSelectionModel<SourceItem> popupSelectionModel; private SelectionListener selectionListener; private FilterPopupPanel filterPopupPanel; private boolean anyFriendFound; /** * Constructs a SourceFilter using the specified results list. */ public SourceFilter(EventList<E> resultsList) { FilterResources resources = getResources(); // Set up visual components. panel.setLayout(new MigLayout("insets 0 0 0 0, gap 0!", "[left,grow]", "")); panel.setOpaque(false); label.setFont(resources.getHeaderFont()); label.setForeground(resources.getHeaderColor()); label.setText(I18n.tr("From")); list.setCellRenderer(new SourceCellRenderer(resources.getBackground(), BorderFactory.createEmptyBorder(1, 7, 0, 7), false)); 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 (filterPopupPanel != null) { filterPopupPanel.setPopupTriggered(true); } } @Override public void mouseExited(MouseEvent e) { if (filterPopupPanel != null) { filterPopupPanel.setPopupTriggered(false); } } }); // Apply results list to filter. initialize(resultsList); // Calculate max list height. list.setPrototypeCellValue("Type"); int listHeight = 3 * list.getFixedCellHeight(); panel.add(label , "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 search results. */ private void initialize(EventList<E> resultsList) { // Create list of unique source values. sourceList = createSourceList(resultsList); uniqueSourceListFactory = new UniqueListFactory<SourceItem>(sourceList, new SourceItemComparator()); uniqueSourceListFactory.setName(I18n.tr("Sources")); uniqueSourceList = uniqueSourceListFactory.getUniqueList(); // Create list of unique friend values. friendList = createFriendList(resultsList); uniqueFriendListFactory = new UniqueListFactory<SourceItem>(friendList, new SourceItemComparator()); uniqueFriendListFactory.setName(I18n.tr("Friends")); uniqueFriendList = uniqueFriendListFactory.getUniqueList(); // Add listener to update unique list in use. uniqueSourceList.addListEventListener(new ListEventListener<SourceItem>() { @Override public void listChanged(ListEvent<SourceItem> listChanges) { updateAnonymousFound(); updateAnyFriendFound(); updateMoreVisibility(); } }); // Add listener to display "more" button when needed. uniqueFriendList.addListEventListener(new ListEventListener<SourceItem>() { @Override public void listChanged(ListEvent<SourceItem> listChanges) { updateMoreVisibility(); } }); // Set unique list for filter. currentUniqueList = uniqueFriendList; updateMoreVisibility(); updateFilterList(); } /** * Updates the filter list using the current unique list. */ private void updateFilterList() { // Dispose of old models and lists. if (selectionListener != null) list.removeListSelectionListener(selectionListener); if (listModel != null) listModel.dispose(); if (selectionModel != null) selectionModel.dispose(); if (sortedList != null) sortedList.dispose(); // Create sorted list using current unique list. sortedList = GlazedListsFactory.sortedList(currentUniqueList, new SourceItemCountComparator()); // Create list and selection models. listModel = new DefaultEventListModel<SourceItem>(sortedList); selectionModel = new DefaultEventSelectionModel<SourceItem>(sortedList); list.setSelectionModel(selectionModel); list.setModel(listModel); // Add selection listener to update filter. selectionListener = new SelectionListener(selectionModel); list.addListSelectionListener(selectionListener); } /** * Adds the specified listener to the list that is notified when the * <code>anyFriendFound</code> state changes. */ public void addFriendListener(FriendListener listener) { friendListenerList.add(listener); } /** * Removes the specified listener from the list that is notified when the * <code>anyFriendFound</code> state changes. */ public void removeFriendListener(FriendListener listener) { friendListenerList.remove(listener); } @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 source and friend lists. uniqueSourceListFactory.dispose(); uniqueFriendListFactory.dispose(); ((TransformedList) sourceList).dispose(); ((TransformedList) friendList).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(anyFriendFound); } /** * Returns a text description of the filter state. */ @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getClass().getSimpleName()).append("["); buf.append("uniqueItems=").append(currentUniqueList.size()); buf.append(", active=").append(isActive()); EventList<SourceItem> selectedList = selectionModel.getSelected(); buf.append(", selection=").append((selectedList.size() > 0) ? selectedList.get(0) : "null"); buf.append("]"); return buf.toString(); } /** * Returns a list of SourceItem objects that represent the sources for the * elements in the specified results list. */ private EventList<SourceItem> createSourceList(EventList<E> resultsList) { // Create collection list model. CollectionList.Model<E, SourceItem> model = new CollectionList.Model<E, SourceItem>() { @Override public List<SourceItem> getChildren(E parent) { List<SourceItem> list = new ArrayList<SourceItem>(); if (parent.isAnonymous()) { list.add(SourceItem.ANONYMOUS_SOURCE); } if (parent.getFriends().size() > 0) { list.add(SourceItem.ANY_FRIEND_SOURCE); } return list; } }; // Create collection list. return GlazedListsFactory.collectionList(resultsList, model); } /** * Returns a list of SourceItem objects that represent the friends for the * elements in the specified results list. */ private EventList<SourceItem> createFriendList(EventList<E> resultsList) { // Create collection list model for friends. CollectionList.Model<E, Friend> model = new CollectionList.Model<E, Friend>() { @Override public List<Friend> getChildren(E parent) { Collection<Friend> friends = parent.getFriends(); return new ArrayList<Friend>(friends); } }; // Create collection list for friends. CollectionList<E, Friend> collectionList = GlazedListsFactory.collectionList(resultsList, model); // Create function list. return GlazedListsFactory.simpleFunctionList(collectionList, new SourceItemFriendFunction()); } /** * Determines whether anonymous sources are found, and updates the current * unique list being displayed. When anonymous sources are found, the * P2P Network/Any Friends list is used; otherwise, the friends-only list * is used. */ private void updateAnonymousFound() { // Determine if anonymous sources are found. boolean found = uniqueSourceList.contains(SourceItem.ANONYMOUS_SOURCE); // Update current unique list only if changed. UniqueList<SourceItem> newList = found ? uniqueSourceList : uniqueFriendList; if (currentUniqueList == newList) return; currentUniqueList = newList; // Update button label. moreButton.getAction().putValue(Action.NAME, found ? I18n.tr("friends") : I18n.tr("more")); // Post Runnable on event queue to update list. Because this method is // handling a listChanged event, we need to post a new event to modify // the list. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateFilterList(); } }); } /** * Determines whether any friend sources are found, and notifies listeners * when the state changes. */ private void updateAnyFriendFound() { // Determine if any friend sources are found. boolean found = uniqueSourceList.contains(SourceItem.ANY_FRIEND_SOURCE); // Update indicator if necessary. if (anyFriendFound == found) return; anyFriendFound = found; // Notify listeners about state change. for (int i = 0, size = friendListenerList.size(); i < size; i++) { friendListenerList.get(i).friendFound(found); } } /** * Updates the visibility of the more button. For the P2P Network/Any * Friends list, the button is always displayed; for the Friends-only list, * the button is displayed when there are more than three friends. */ private void updateMoreVisibility() { boolean visible = (currentUniqueList == uniqueSourceList) || (uniqueFriendList.size() > 3); if (!moreButton.isVisible() && visible) { moreButton.setVisible(true); } else if (moreButton.isVisible() && !visible) { moreButton.setVisible(false); } } /** * Creates a popup to display the complete list of friends. */ private FilterPopupPanel createFriendPopup() { FilterPopupPanel popupPanel = new FilterPopupPanel(getResources(), I18n.tr("Friends")); // Set list cell renderer. popupPanel.setListCellRenderer(new SourceCellRenderer(popupPanel.getBackground(), BorderFactory.createEmptyBorder(1, 4, 0, 1), true)); // Set list and selection models. We use the unique list directly // to display values alphabetically. DefaultEventListModel<SourceItem> listModel = new DefaultEventListModel<SourceItem>(uniqueFriendList); popupSelectionModel = new DefaultEventSelectionModel<SourceItem>(uniqueFriendList); popupPanel.setListModel(listModel); popupPanel.setListSelectionModel(popupSelectionModel); // Add selection listener to update filter. popupSelectionModel.addListSelectionListener(new SelectionListener(popupSelectionModel)); return popupPanel; } /** * Displays the "friend" popup that lists all friends. */ private void showFriendPopup() { if (filterPopupPanel == null) { filterPopupPanel = createFriendPopup(); } filterPopupPanel.showPopup(moreButton, list.getWidth() - 12, label.getY() - moreButton.getY()); } /** * Hides the "friend" popup that lists all friends. */ private void hideFriendPopup() { if (filterPopupPanel != null) { filterPopupPanel.hidePopup(); } } /** * Action to display list of all friends in popup window. */ private class MoreAction extends AbstractAction { public MoreAction() { super(I18n.tr("more")); } @Override public void actionPerformed(ActionEvent e) { if (filterPopupPanel == null) { showFriendPopup(); } else if (filterPopupPanel.isPopupReady()) { showFriendPopup(); } else { filterPopupPanel.setPopupReady(true); } } } /** * Listener to handle selection changes to update the matcher editor. */ private class SelectionListener implements ListSelectionListener { private final DefaultEventSelectionModel<SourceItem> selectionModel; public SelectionListener(DefaultEventSelectionModel<SourceItem> 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<SourceItem> selectedList = selectionModel.getSelected(); if (selectedList.size() > 0) { SourceItem value = selectedList.get(0); // Create new matcher and activate. Matcher<E> newMatcher = new SourceMatcher<E>(value); activate(value.toString(), newMatcher); } else { // Deactivate to clear matcher. deactivate(); } // Hide popup if showing. hideFriendPopup(); // Notify filter listeners. fireFilterChanged(SourceFilter.this); } } /** * Cell renderer for SourceItem values. Note that the filter list uses the * current unique list, while the popup list always uses the unique friend * list. */ private class SourceCellRenderer extends DefaultListCellRenderer { private final Color background; private final Border border; private final boolean useFriend; public SourceCellRenderer(Color background, Border border, boolean useFriend) { this.background = background; this.border = border; this.useFriend = useFriend; } @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 instanceof SourceItem)) { // Get count. int count = useFriend ? uniqueFriendList.getCount((SourceItem) value) : currentUniqueList.getCount((SourceItem) value); // Set text. StringBuilder buf = new StringBuilder(); buf.append(((SourceItem) value).getName()); buf.append(" (").append(count).append(")"); ((JLabel) renderer).setText(buf.toString()); // Set appearance. ((JLabel) renderer).setBackground(background); ((JLabel) renderer).setBorder(border); } return renderer; } } /** * A Comparator for SourceItem values. */ private static class SourceItemComparator implements Comparator<SourceItem> { @Override public int compare(SourceItem item1, SourceItem item2) { if (item1.getType() == item2.getType()) { String name1 = item1.getName(); String name2 = item2.getName(); return Objects.compareToNullIgnoreCase(name1, name2, false); } else if (item1.getType() == SourceItem.Type.ANONYMOUS) { return 1; } else if (item2.getType() == SourceItem.Type.ANONYMOUS) { return -1; } else if (item1.getType() == SourceItem.Type.ANY_FRIEND) { return 1; } else { return -1; } } } /** * A Comparator to sort SourceItem values by their result count. */ private class SourceItemCountComparator implements Comparator<SourceItem> { @Override public int compare(SourceItem item1, SourceItem item2) { int count1 = currentUniqueList.getCount(item1); int count2 = currentUniqueList.getCount(item2); // Return inverse value to sort in descending order. return (count1 < count2) ? 1 : ((count1 > count2) ? -1 : 0); } } /** * A function to transform a list of friends into a list of source * items. */ private static class SourceItemFriendFunction implements Function<Friend, SourceItem> { @Override public SourceItem evaluate(Friend sourceValue) { return new SourceItem(SourceItem.Type.FRIEND, sourceValue.getRenderName()); } } /** * Defines a listener for friend found events. */ public static interface FriendListener { void friendFound(boolean found); } }