package org.limewire.ui.swing.search; import java.awt.AWTEvent; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import javax.swing.BorderFactory; import javax.swing.DefaultListCellRenderer; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.UIManager; import javax.swing.border.Border; import net.miginfocom.swing.MigLayout; import org.jdesktop.application.Resource; import org.limewire.collection.AutoCompleteDictionary; import org.limewire.collection.NECallable; import org.limewire.concurrent.ExecutorsHelper; import org.limewire.concurrent.ListeningExecutorService; import org.limewire.concurrent.ListeningFuture; import org.limewire.concurrent.SimpleFuture; import org.limewire.core.api.search.SearchCategory; import org.limewire.ui.swing.components.AutoCompleter; import org.limewire.ui.swing.components.CollectionBackedListModel; import org.limewire.ui.swing.components.ExtendedCompoundBorder; import org.limewire.ui.swing.components.SideLineBorder; import org.limewire.ui.swing.util.FontUtils; import org.limewire.ui.swing.util.GuiUtils; import org.limewire.ui.swing.util.I18n; import org.limewire.ui.swing.util.SwingUtils; import com.google.inject.Inject; /** An autocompleter that shows its suggestions in a list and can have new suggestions added. */ public class HistoryAndFriendAutoCompleter implements AutoCompleter { private final ListeningExecutorService queue = ExecutorsHelper.newProcessingQueue("AutoCompleteQueue"); @Resource private Color selectionBackground; @Resource private Font font; private ListeningFuture<Boolean> lastFuture; private final JPanel entryPanel; private AutoCompleterCallback callback; private String currentText; private boolean showSuggestions = true; private AutoCompleteDictionary historyDictionary; private SmartAutoCompleteDictionary smartDictionary; private final AutoCompleteList entryList; private final KeywordAssistedSearchBuilder keywordSearchBuilder; @Inject public HistoryAndFriendAutoCompleter(KeywordAssistedSearchBuilder keywordSearchBuilder) { this.keywordSearchBuilder = keywordSearchBuilder; GuiUtils.assignResources(this); entryPanel = new JPanel(new MigLayout("insets 0, gap 0, fill")); entryPanel.setBorder(BorderFactory.createLineBorder(Color.BLACK)); entryPanel.setBackground(UIManager.getColor("List.background")); entryList = new AutoCompleteList(); JScrollPane entryScrollPane = new JScrollPane(entryList); entryScrollPane.setBorder(BorderFactory.createEmptyBorder()); entryScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); entryScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); entryPanel.add(entryScrollPane, "grow"); } public void setSuggestionsShown(boolean value) { this.showSuggestions = value; } public void setHistoryDictionary(AutoCompleteDictionary dictionary) { this.historyDictionary = dictionary; } public void setSmartDictionary(SmartAutoCompleteDictionary dictionary) { this.smartDictionary = dictionary; } @Override public void setAutoCompleterCallback(AutoCompleterCallback callback) { this.callback = callback; } @Override public boolean isAutoCompleteAvailable() { return entryList.getModel().getSize() != 0; } @Override public void decrementSelection() { entryList.decrementSelection(); } @Override public JComponent getRenderComponent() { return entryPanel; } @Override public String getSelectedAutoCompleteString() { Object selection = entryList.getSelectedValue(); if(selection != null) { return selection.toString(); } else { return null; } } @Override public void incrementSelection() { entryList.incrementSelection(); } @Override public ListeningFuture<Boolean> setInput(String input) { if(input == null) { input = ""; } currentText = input; // Only if both are immediate can we immediately show. if (historyDictionary.isImmediate() && smartDictionary.isImmediate()) { try { lookupAndSetItems(currentText); } catch(InterruptedException error) { throw new IllegalStateException(error); } return new SimpleFuture<Boolean>(true); } else { if(lastFuture != null) { lastFuture.cancel(true); } setItems(Collections.<Entry>emptyList()); final String lookupText = currentText; lastFuture = queue.submit(new NECallable<Boolean>() { @Override public Boolean call() { if(Thread.interrupted()) { return false; } try { lookupAndSetItems(lookupText); return true; } catch (InterruptedException ignored) { return false; } } }); return lastFuture; } } /** * Builds a list of auto-completion entries and sets them in the * displayable list. */ private void lookupAndSetItems(final String lookupText) throws InterruptedException { // Create list of items. Collection<String> histories = historyDictionary.getPrefixedBy(lookupText); final ArrayList<Entry> items = new ArrayList<Entry>(histories.size()); // Add search history items. boolean needFirstHistory = true; for (String string : histories) { if (needFirstHistory) { items.add(new Entry(string, Entry.Reason.FIRST_HISTORY)); needFirstHistory = false; } else { items.add(new Entry(string, Entry.Reason.HISTORY)); } } // Add smart suggestion items. if (showSuggestions) { Collection<SmartQuery> suggestions = smartDictionary.getPrefixedBy(lookupText); SearchCategory category = smartDictionary.getSearchCategory(); items.ensureCapacity(items.size() + suggestions.size()); boolean needFirstSuggestion = true; for (SmartQuery query : suggestions) { String queryText = keywordSearchBuilder.createCompositeQuery(query.getQueryData(), category); if (needFirstSuggestion) { items.add(new Entry(queryText, query.toString(), Entry.Reason.FIRST_SUGGESTION)); needFirstSuggestion = false; } else { items.add(new Entry(queryText, query.toString(), Entry.Reason.SUGGESTION)); } } } SwingUtils.invokeNowOrWaitWithInterrupted(new Runnable() { @Override public void run() { setItems(items); } }); } /** * Updates the auto-completion list with the specified entries. */ private void setItems(Collection<Entry> items) { entryList.setModel(new CollectionBackedListModel(items)); entryList.setVisibleRowCount(Math.min(8, items.size())); } /** A list that's used to show auto-complete items. */ private class AutoCompleteList extends JList { AutoCompleteList() { enableEvents(AWTEvent.MOUSE_EVENT_MASK); setSelectionMode(ListSelectionModel.SINGLE_SELECTION); setFocusable(false); setCellRenderer(new Renderer()); setFont(font); setSelectionBackground(selectionBackground); setSelectionForeground(Color.BLACK); } // override to return true always, to enforce '...' added @Override public boolean getScrollableTracksViewportWidth() { return true; } /** * Sets the text field's selection with the clicked item. */ @Override protected void processMouseEvent(MouseEvent me) { super.processMouseEvent(me); if(me.getID() == MouseEvent.MOUSE_CLICKED) { int idx = locationToIndex(me.getPoint()); if(idx != -1 && isSelectedIndex(idx)) { callback.itemSuggested(getSelectedValue().toString(), false, true); } } } /** * Increments the selection by one. */ void incrementSelection() { if(getSelectedIndex() == getModel().getSize() - 1) { callback.itemSuggested(currentText, true, false); clearSelection(); } else { int selectedIndex = getSelectedIndex() + 1; setSelectedIndex(selectedIndex); ensureIndexIsVisible(selectedIndex); callback.itemSuggested(getSelectedValue().toString(), true, false); } } /** * Decrements the selection by one. */ void decrementSelection() { if(getSelectedIndex() == 0) { callback.itemSuggested(currentText, true, false); clearSelection(); } else { int selectedIndex = getSelectedIndex(); if(selectedIndex == -1) { selectedIndex = getModel().getSize() - 1; } else { selectedIndex--; } setSelectedIndex(selectedIndex); ensureIndexIsVisible(selectedIndex); if (getSelectedValue() != null) { callback.itemSuggested(getSelectedValue().toString(), true, false); } } } } /** * Cell renderer for entries in the auto-completion list. */ private static class Renderer extends DefaultListCellRenderer { private final ExtendedCompoundBorder compoundBorder; private final JPanel firstSuggestionPanel; private final DefaultListCellRenderer firstSuggestionLabel; private final JLabel firstSuggestionTitle; private final Border firstSuggestionBorder = new SideLineBorder(Color.GRAY, SideLineBorder.Side.TOP); public Renderer() { compoundBorder = new ExtendedCompoundBorder(BorderFactory.createEmptyBorder(), BorderFactory.createEmptyBorder(0, 2, 0, 2)); firstSuggestionPanel = new JPanel(); firstSuggestionPanel.setLayout(new MigLayout("fill, gap 0, insets 0")); firstSuggestionPanel.setBorder(compoundBorder); firstSuggestionLabel = new DefaultListCellRenderer(); firstSuggestionTitle = new DefaultListCellRenderer(); FontUtils.changeSize(firstSuggestionTitle, -1); firstSuggestionTitle.setForeground(Color.GRAY); firstSuggestionPanel.add(firstSuggestionLabel, "alignx left, grow, wmin 0, gapright 10"); firstSuggestionPanel.add(firstSuggestionTitle, "alignx right"); } @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { String render; if(value == null) { render = ""; } else { render = value.toString(); if(value instanceof Entry) { render = ((Entry) value).getDisplayText(); if(((Entry)value).reason == Entry.Reason.FIRST_HISTORY || ((Entry)value).reason == Entry.Reason.FIRST_SUGGESTION) { firstSuggestionLabel.getListCellRendererComponent(list, render, index, isSelected, cellHasFocus); firstSuggestionLabel.setBorder(BorderFactory.createEmptyBorder()); if(isSelected) { firstSuggestionTitle.setForeground(list.getSelectionForeground()); } else { firstSuggestionTitle.setForeground(Color.GRAY); } if(index != 0) { compoundBorder.setOuterBorder(firstSuggestionBorder); } else { compoundBorder.setOuterBorder(BorderFactory.createEmptyBorder()); } if (((Entry)value).reason == Entry.Reason.FIRST_HISTORY) { firstSuggestionTitle.setText(I18n.tr("History")); } else { firstSuggestionTitle.setText(I18n.tr("Smart Search")); } firstSuggestionTitle.setBackground(firstSuggestionLabel.getBackground()); firstSuggestionPanel.setBackground(firstSuggestionLabel.getBackground()); return firstSuggestionPanel; } } } super.getListCellRendererComponent(list, render, index, isSelected, cellHasFocus); compoundBorder.setOuterBorder(getBorder()); setBorder(compoundBorder); return this; } } /** * An entry in the auto-completion list. */ private static class Entry { private static enum Reason { FIRST_HISTORY, HISTORY, FIRST_SUGGESTION, SUGGESTION }; private final String query; private final String display; private final Reason reason; public Entry(String query, Reason reason) { this(query, query, reason); } public Entry(String query, String display, Reason reason) { this.query = query; this.display = display; this.reason = reason; } public String getDisplayText() { return display; } @Override public String toString() { return query; } @Override public boolean equals(Object obj) { if(obj == this) { return true; } else { return ((Entry)obj).query.equals(query) && ((Entry)obj).reason == reason; } } } }