package org.limewire.ui.swing.search;
import java.awt.Color;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.LinkedList;
import java.util.List;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.jdesktop.swingx.JXPanel;
import org.limewire.collection.AutoCompleteDictionary;
import org.limewire.core.api.search.SearchCategory;
import org.limewire.core.settings.SearchSettings;
import org.limewire.friend.api.FriendConnectionEvent;
import org.limewire.listener.EventListener;
import org.limewire.listener.ListenerSupport;
import org.limewire.listener.SwingEDTEvent;
import org.limewire.setting.evt.SettingEvent;
import org.limewire.setting.evt.SettingListener;
import org.limewire.ui.swing.action.AbstractAction;
import org.limewire.ui.swing.components.DropDownListAutoCompleteControl;
import org.limewire.ui.swing.components.IconButton;
import org.limewire.ui.swing.components.LimeComboBox;
import org.limewire.ui.swing.components.PromptTextField;
import org.limewire.ui.swing.components.LimeComboBox.SelectionListener;
import org.limewire.ui.swing.components.decorators.ComboBoxDecorator;
import org.limewire.ui.swing.components.decorators.TextFieldDecorator;
import org.limewire.ui.swing.painter.BorderPainter.AccentType;
import org.limewire.ui.swing.search.advanced.AdvancedPopupPanel;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.ui.swing.util.CategoryIconManager;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.ui.swing.util.SwingUtils;
import org.limewire.util.I18NConvert;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
/**
* The search bar component at the top of the UI main window. This includes
* the drop-down search category box, search input field, and search button.
*/
public class SearchBar extends JXPanel {
@Resource private Color searchBorder;
private final LimeComboBox comboBox;
private final PromptTextField searchField;
private final IconButton searchButton;
private final HistoryAndFriendAutoCompleter autoCompleter;
private final SmartAutoCompleteFactory smartAutoCompleteFactory;
private final AutoCompleteDictionary searchHistory;
private final Provider<AdvancedPopupPanel> advancedPopupProvider;
private final Action advancedAction;
private SearchCategory categoryToSearch;
private static volatile int categorySwitched = 0;
@Inject
public SearchBar(ComboBoxDecorator comboBoxDecorator,
SmartAutoCompleteFactory smartAutoCompleteFactory,
@Named("searchHistory") AutoCompleteDictionary searchHistory,
final HistoryAndFriendAutoCompleter autoCompleter,
Provider<AdvancedPopupPanel> advancedPopupProvider,
CategoryIconManager categoryIconManager,
TextFieldDecorator textFieldDecorator) {
super(new MigLayout("ins 0, gapx 0, gapy 0"));
GuiUtils.assignResources(this);
this.smartAutoCompleteFactory = smartAutoCompleteFactory;
this.searchHistory = searchHistory;
this.autoCompleter = autoCompleter;
this.advancedPopupProvider = advancedPopupProvider;
this.categoryToSearch = SearchCategory.forId(SwingUiSettings.DEFAULT_SEARCH_CATEGORY_ID.getValue());
this.advancedAction = new AdvancedAction();
Action actionToSelect = null;
List<Action> typeActions = new LinkedList<Action>();
for (SearchCategory cat : SearchCategory.values()) {
if (cat == SearchCategory.OTHER) {
continue;
}
Icon icon = null;
if(cat != SearchCategory.ALL) {
icon = categoryIconManager.getIcon(cat.getCategory());
}
Action action = new CategoryAction(cat, icon);
if (cat == this.categoryToSearch) {
actionToSelect = action;
}
typeActions.add(action);
}
typeActions.add(advancedAction);
comboBox = new LimeComboBox(typeActions);
comboBoxDecorator.decorateLightFullComboBox(comboBox);
comboBox.setName("SearchBar.comboBox");
// Disable fields when Advanced Search selected.
comboBox.addSelectionListener(new SelectionListener() {
@Override
public void selectionChanged(Action action) {
boolean advanced = action instanceof AdvancedAction;
searchField.setEnabled(!advanced);
searchButton.setEnabled(!advanced);
}
});
searchField = new PromptTextField(I18n.tr("Search..."));
textFieldDecorator.decoratePromptField(searchField, AccentType.BUBBLE, searchBorder);
searchField.setName("SearchBar.searchField");
searchField.setDocument(new SearchFieldDocument());
// Show Advanced Search popup when field disabled.
searchField.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (!searchField.isEnabled()) {
advancedAction.actionPerformed(null);
}
}
});
searchButton = new IconButton();
searchButton.removeActionHandListener();
searchButton.setName("SearchBar.searchButton");
searchButton.setFocusPainted(false);
searchButton.setToolTipText(I18n.tr("Search P2P Network"));
final DropDownListAutoCompleteControl autoCompleteControl = DropDownListAutoCompleteControl.install(this.searchField, autoCompleter);
autoCompleteControl.setAutoComplete(true);
autoCompleter.setHistoryDictionary(searchHistory);
addSearchActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// Get search text, and add to history if non-empty.
String searchText = getSearchText();
if (!searchText.isEmpty() && SwingUiSettings.KEEP_SEARCH_HISTORY.getValue()) {
SearchBar.this.searchHistory.addEntry(searchText);
}
}
});
SwingUiSettings.SHOW_SMART_SUGGESTIONS.addSettingListener(new SettingListener() {
@Override
public void settingChanged(SettingEvent evt) {
SwingUtils.invokeNowOrLater(new Runnable() {
@Override
public void run() {
autoCompleter.setSuggestionsShown(SwingUiSettings.SHOW_SMART_SUGGESTIONS.getValue());
}
});
}
});
autoCompleter.setSuggestionsShown(SwingUiSettings.SHOW_SMART_SUGGESTIONS.getValue());
if (actionToSelect != null) {
autoCompleter.setSmartDictionary(smartAutoCompleteFactory.create(categoryToSearch));
comboBox.setSelectedAction(actionToSelect);
} else {
autoCompleter.setSmartDictionary(smartAutoCompleteFactory.create(SearchCategory.ALL));
}
setOpaque(false);
add(comboBox);
add(searchField, "gap 5");
add(searchButton, "gap 5");
}
/**
* Performs additional Guice injection tasks. This method adds a listener
* to handle XMPP connection events.
*/
@Inject
void register(ListenerSupport<FriendConnectionEvent> connectionSupport) {
connectionSupport.addListener(new EventListener<FriendConnectionEvent>() {
@Override
@SwingEDTEvent
public void handleEvent(FriendConnectionEvent event) {
switch (event.getType()) {
case CONNECTED:
searchButton.setToolTipText(I18n.tr("Search P2P Network and Friends"));
break;
default:
searchButton.setToolTipText(I18n.tr("Search P2P Network"));
break;
}
}
});
}
public boolean requestSearchFocus() {
// Request w/ temporary==true to indicate we don't
// want to select everything.
return searchField.requestFocus(true);
}
public void selectAllSearchText() {
searchField.setCaretPosition(searchField.getDocument().getLength());
searchField.selectAll();
}
/**
* Sets the search text.
*/
public void setText(String text) {
searchField.setText(text);
}
/**
* Sets the selected category in the combo box.
*/
public void setCategory(SearchCategory category) {
for ( Action a : comboBox.getActions() ) {
if (a instanceof CategoryAction) {
CategoryAction categoryAction = (CategoryAction) a;
if (categoryAction.getCategory() == category) {
comboBox.selectAction(categoryAction);
return;
}
}
}
}
public void addSearchActionListener(ActionListener actionListener) {
searchField.addActionListener(actionListener);
searchButton.addActionListener(actionListener);
}
/**
* Returns the search text with leading and trailing whitespace removed.
* The method returns an empty string if there are no non-whitespace
* characters.
*/
public String getSearchText() {
// Get search text and trim whitespace.
String searchText = searchField.getText();
return searchText != null ? searchText.trim() : "";
}
public SearchCategory getCategory() {
return categoryToSearch;
}
/**
* Selects the advanced search option.
*/
public void selectAdvancedSearch() {
comboBox.selectAction(advancedAction);
}
/**
* Action to display the advanced search popup dialog.
*/
private class AdvancedAction extends AbstractAction {
AdvancedAction() {
super(I18n.tr("Advanced"));
putValue(AbstractAction.SEPARATOR, true);
}
@Override
public void actionPerformed(ActionEvent e) {
// Display popup dialog.
advancedPopupProvider.get().showPopup(comboBox, searchField.getX(),
searchField.getY() + searchField.getHeight() - 1, searchField.getWidth());
}
}
/**
* Action representing a search category.
*/
private class CategoryAction extends AbstractAction {
private final SearchCategory category;
CategoryAction(SearchCategory category, Icon icon) {
super(SearchCategoryUtils.getName(category));
putValue(SMALL_ICON, icon);
this.category = category;
}
@Override
public void actionPerformed(ActionEvent arg0) {
categoryToSearch = category;
autoCompleter.setSmartDictionary(smartAutoCompleteFactory.create(category));
categorySwitched++;
}
public SearchCategory getCategory() {
return category;
}
}
/**
* Helper class that filters out all characters that make the search longer
* than the maximum allowed length. If characters entered make the search
* field too long (normalized or not normalized), the system should beep.
*/
private static class SearchFieldDocument extends PlainDocument {
private static final int MAX_QUERY_LENGTH =
SearchSettings.MAX_QUERY_LENGTH.getValue();
@Override
public void insertString(int offs,
String str,
AttributeSet a) throws BadLocationException {
if(str == null) {
return;
}
if(offs >= MAX_QUERY_LENGTH) {
Toolkit.getDefaultToolkit().beep();
return;
}
// Normalized String are maybe longer or shorter than MAX_QUERY_LENGTH
String norm = I18NConvert.instance().getNorm(str);
if (getMaxLength() + Math.max(str.length(), norm.length()) > MAX_QUERY_LENGTH) {
Toolkit.getDefaultToolkit().beep();
return;
}
super.insertString(offs, str, a);
}
/**
* Returns the maximum length of the existing text normalized or not
* normalized.
*/
private int getMaxLength() {
try {
String text = getText(0, getLength());
return Math.max(text.length(), I18NConvert.instance().getNorm(text).length());
} catch (BadLocationException e) {
return 0;
}
}
}
}