package org.limewire.ui.swing.mainframe;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.jdesktop.swingx.JXButton;
import org.jdesktop.swingx.JXPanel;
import org.limewire.core.api.search.Search;
import org.limewire.core.api.search.SearchCategory;
import org.limewire.core.api.search.SearchListener;
import org.limewire.core.api.search.SearchResult;
import org.limewire.core.api.search.SearchDetails.SearchType;
import org.limewire.core.api.search.browse.BrowseSearch;
import org.limewire.ui.swing.components.Disposable;
import org.limewire.ui.swing.components.FlexibleTabList;
import org.limewire.ui.swing.components.FlexibleTabListFactory;
import org.limewire.ui.swing.components.NoOpAction;
import org.limewire.ui.swing.components.SelectableJXButton;
import org.limewire.ui.swing.components.TabActionMap;
import org.limewire.ui.swing.components.decorators.ButtonDecorator;
import org.limewire.ui.swing.friends.refresh.AllFriendsRefreshManager;
import org.limewire.ui.swing.home.HomeMediator;
import org.limewire.ui.swing.library.LibraryMediator;
import org.limewire.ui.swing.nav.NavCategory;
import org.limewire.ui.swing.nav.NavItem;
import org.limewire.ui.swing.nav.NavItemListener;
import org.limewire.ui.swing.nav.NavMediator;
import org.limewire.ui.swing.nav.NavSelectable;
import org.limewire.ui.swing.nav.NavigationListener;
import org.limewire.ui.swing.nav.Navigator;
import org.limewire.ui.swing.nav.NavigatorUtils;
import org.limewire.ui.swing.painter.factories.BarPainterFactory;
import org.limewire.ui.swing.painter.factories.SearchTabPainterFactory;
import org.limewire.ui.swing.search.DefaultSearchInfo;
import org.limewire.ui.swing.search.DefaultSearchRepeater;
import org.limewire.ui.swing.search.KeywordAssistedSearchBuilder;
import org.limewire.ui.swing.search.SearchBar;
import org.limewire.ui.swing.search.SearchHandler;
import org.limewire.ui.swing.search.SearchInfo;
import org.limewire.ui.swing.search.SearchNavItem;
import org.limewire.ui.swing.search.SearchNavigator;
import org.limewire.ui.swing.search.SearchResultMediator;
import org.limewire.ui.swing.search.KeywordAssistedSearchBuilder.CategoryOverride;
import org.limewire.ui.swing.search.model.SearchResultsModel;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.util.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@Singleton
class TopPanel extends JXPanel implements SearchNavigator {
@Resource private Icon browseIcon;
private final JXButton friendButton;
private final SearchBar searchBar;
private final FlexibleTabList searchList;
private final Navigator navigator;
private final NavItem homeNav;
private final NavItem libraryNav;
private final Provider<KeywordAssistedSearchBuilder> keywordAssistedSearchBuilder;
private final HomeMediator homeMediator;
private final String repeatSearchTitle = I18n.tr("Repeat Search");
private final String refreshBrowseTitle = I18n.tr("Refresh");
/** Maximum number of tabs open in this session*/
private volatile long maxTabCount = 0;
private final AllFriendsRefreshManager allFriendsRefreshManager;
@Inject
public TopPanel(SearchHandler searchHandler,
Navigator navigator,
final HomeMediator homeMediator,
SearchBar searchBar,
FlexibleTabListFactory tabListFactory,
BarPainterFactory barPainterFactory,
SearchTabPainterFactory tabPainterFactory,
final LibraryMediator myLibraryMediator,
Provider<KeywordAssistedSearchBuilder> keywordAssistedSearchBuilder,
Provider<FriendsButton> friendsButtonProvider,
AllFriendsRefreshManager allFriendsRefreshManager,
ButtonDecorator buttonDecorator) {
GuiUtils.assignResources(this);
this.searchBar = searchBar;
this.navigator = navigator;
this.searchBar.addSearchActionListener(new Searcher(searchHandler));
this.keywordAssistedSearchBuilder = keywordAssistedSearchBuilder;
this.homeMediator = homeMediator;
this.allFriendsRefreshManager = allFriendsRefreshManager;
setName("WireframeTop");
setBackgroundPainter(barPainterFactory.createTopBarPainter());
homeNav = navigator.createNavItem(NavCategory.LIMEWIRE, HomeMediator.NAME, homeMediator);
libraryNav = navigator.createNavItem(NavCategory.LIBRARY, LibraryMediator.NAME, myLibraryMediator);
JXButton libraryButton = new SelectableJXButton(NavigatorUtils.getNavAction(libraryNav));
libraryButton.setName("WireframeTop.libraryButton");
libraryButton.setText(I18n.tr("My Files"));
buttonDecorator.decorateBasicHeaderButton(libraryButton);
friendButton = friendsButtonProvider.get();
friendButton.setName("WireframeTop.friendsButton");
searchList = tabListFactory.create();
searchList.setName("WireframeTop.SearchList");
searchList.setCloseAllText(I18n.tr("Close All Searches"));
searchList.setCloseOneText(I18n.tr("Close Search"));
searchList.setCloseOtherText(I18n.tr("Close Other Searches"));
searchList.setRemovable(true);
searchList.setSelectionPainter(tabPainterFactory.createSelectionPainter());
searchList.setHighlightPainter(tabPainterFactory.createHighlightPainter());
searchList.setNormalPainter(tabPainterFactory.createNormalPainter());
searchList.setTabInsets(new Insets(0,10,2,10));
setLayout(new MigLayout("gap 0, insets 0, fill, alignx leading"));
add(libraryButton, "gapleft 5, gapbottom 2, gaptop 0");
add(friendButton, "gapleft 3, gapbottom 2, gaptop 0");
add(searchBar, "gapleft 11, gapbottom 2, gaptop 1");
add(searchList.getComponent(), "gapleft 10, gaptop 4, gapbottom 0, grow, push");
navigator.addNavigationListener(new NavigationListener() {
@Override
public void categoryRemoved(NavCategory category, boolean wasSelected) {
if(wasSelected && category == NavCategory.SEARCH_RESULTS) {
goHome();
}
}
@Override public void categoryAdded(NavCategory category) {}
@Override public void itemAdded(NavCategory category, NavItem navItem) {}
@Override public void itemRemoved(NavCategory category, NavItem navItem, boolean wasSelected) {}
@Override public void itemSelected(NavCategory category, NavItem navItem, NavSelectable selectable, NavMediator navMediator) {
if(category != NavCategory.SEARCH_RESULTS) {
TopPanel.this.searchBar.setText("");
}
}
});
}
@Override
public boolean requestFocusInWindow() {
return searchBar.requestFocusInWindow();
}
@Override
public SearchNavItem addSearch(String title, final JComponent searchPanel, final BrowseSearch search, SearchResultsModel model) {
return addSearch(title, searchPanel, search, model, createBrowseActions(search, model), browseIcon, false);
}
@Override
public SearchNavItem addSearch(String title, final JComponent searchPanel, final Search search, SearchResultsModel model) {
return addSearch(title, searchPanel, search, model, createSearchActions(search, model), null, true);
}
@Override
public void openAdvancedSearch() {
searchBar.selectAdvancedSearch();
}
private SearchNavItem addSearch(String title, final JComponent searchPanel, final Search search, SearchResultsModel model,
List<Action> contextActions, Icon icon, boolean stopSpinnerAfter50Results) {
NavItem item = navigator.createNavItem(NavCategory.SEARCH_RESULTS, title, new SearchResultMediator(searchPanel));
SearchAction action = new SearchAction(item);
action.putValue(Action.LARGE_ICON_KEY, icon);
search.addSearchListener(action);
Action moreTextAction = new NoOpAction();
action.putValue(Action.LONG_DESCRIPTION, action.getValue(Action.NAME));
TabActionMap actionMap = new TabActionMap(action, action, moreTextAction, contextActions);
searchList.addTabActionMapAt(actionMap, 0);
maybeIncrementMaxTabCount();
String searchText = "";
if(model.getSearchType() == SearchType.KEYWORD) {
searchText = title;
}
item.addNavItemListener(new SearchTabNavItemListener(action, actionMap, (Disposable)searchPanel, searchText, search.getCategory()));
return new SearchNavItemImpl(item, action, stopSpinnerAfter50Results);
}
public void goHome() {
homeNav.select();
homeMediator.getComponent().loadDefaultUrl();
}
private List<Action> createSearchActions(Search search, SearchResultsModel model){
final Action moreResults = new MoreResultsAction(search).register();
final Action repeatSearch = new RepeatSearchAction(repeatSearchTitle, search, model).register();
final Action stopSearch = new StopSearchAction(search).register();
return Arrays.asList(stopSearch, repeatSearch, TabActionMap.SEPARATOR, moreResults);
}
private List<Action> createBrowseActions(Search search, SearchResultsModel model){
final Action refresh = new RepeatSearchAction(refreshBrowseTitle, search, model).register();
return Arrays.asList(refresh);
}
/** Keeps track of the maximum number of tabs open in this session for inspections*/
private void maybeIncrementMaxTabCount() {
if(searchList.getTabs().size() > maxTabCount){
maxTabCount = searchList.getTabs().size();
}
}
/**
* Specialized NavItemListener that handles disposing the SearchPanel after a search is closed,
* removes the panel from the search list. and updates the search bar text and category when a
* new search tab is selected.
*/
private class SearchTabNavItemListener implements NavItemListener {
private final SearchAction action;
private final TabActionMap actionMap;
private final String searchText;
private final Disposable panel;
private final SearchCategory category;
private SearchTabNavItemListener(SearchAction action, TabActionMap actionMap, Disposable panel, String searchText, SearchCategory category) {
this.action = action;
this.actionMap = actionMap;
this.searchText = searchText;
this.panel = panel;
this.category = category;
}
@Override
public void itemRemoved(boolean wasSelected) {
searchList.removeTabActionMap(actionMap);
panel.dispose();
if(searchList.getTabs().size() == 0){
searchBar.setText(null);
}
}
@Override
public void itemSelected(boolean selected) {
searchBar.setText(searchText);
searchBar.setCategory(category);
action.putValue(Action.SELECTED_KEY, selected);
}
}
private final static class SearchNavItemImpl implements SearchNavItem {
private final NavItem item;
private final SearchAction action;
private final boolean stopSpinnerAfter50Results;
private SearchNavItemImpl(NavItem item, SearchAction action, boolean stopSpinnerAfter50Results) {
this.item = item;
this.action = action;
this.stopSpinnerAfter50Results = stopSpinnerAfter50Results;
}
@Override
public void sourceCountUpdated(int newSourceCount) {
if(!item.isSelected()) {
action.putValue(TabActionMap.NEW_HINT, true);
}
if (stopSpinnerAfter50Results && newSourceCount >= 50) {
action.killBusy();
}
}
@Override
public String getId() {
return item.getId();
}
@Override
public void remove() {
item.remove();
}
@Override
public void select() {
select(null);
}
@Override
public void select(NavSelectable selectable) {
item.select();
}
@Override
public void addNavItemListener(NavItemListener listener) {
item.addNavItemListener(listener);
}
@Override
public void removeNavItemListener(NavItemListener listener) {
item.removeNavItemListener(listener);
}
@Override
public boolean isSelected() {
return item.isSelected();
}
}
private class Searcher implements ActionListener {
private final SearchHandler searchHandler;
Searcher(SearchHandler searchHandler) {
this.searchHandler = searchHandler;
}
@Override
public void actionPerformed(ActionEvent e) {
// Get search text, and do search if non-empty.
String searchText = searchBar.getSearchText();
if (!searchText.isEmpty()) {
SearchCategory category = searchBar.getCategory();
String query = searchText;
// Check if the category was overridden by a keyword
CategoryOverride categoryOverride = keywordAssistedSearchBuilder.get().parseCategoryOverride(query);
// Do not allow searches in the Other category
if (categoryOverride != null && categoryOverride.getCategory() != SearchCategory.OTHER) {
// Set new category and trim the category text out of the search string
// for the actual search
category = categoryOverride.getCategory();
query = categoryOverride.getCutQuery();
// Update the UI with the new category
searchBar.setCategory(category);
}
// Attempt to parse an advanced search from the search query
SearchInfo search = keywordAssistedSearchBuilder.get().attemptToCreateAdvancedSearch(
query, category);
// Fall back on the normal search
if (search == null) {
search = DefaultSearchInfo.createKeywordSearch(query, category);
}
if(searchHandler.doSearch(search)) {
searchBar.selectAllSearchText();
}
}
}
}
private class SearchAction extends AbstractAction implements SearchListener {
private final NavItem item;
private Timer busyTimer;
public SearchAction(NavItem item) {
super(item.getId());
this.item = item;
// Make sure this syncs up with any changes in selection.
addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(Action.SELECTED_KEY)) {
if (evt.getNewValue().equals(Boolean.TRUE)) {
SearchAction.this.item.select();
putValue(TabActionMap.NEW_HINT, null);
}
}
}
});
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals(TabActionMap.SELECT_COMMAND)) {
item.select();
if(!StringUtils.isEmpty(searchBar.getSearchText())) {
searchBar.requestSearchFocus();
}
} else if (e.getActionCommand().equals(TabActionMap.REMOVE_COMMAND)) {
item.remove();
}
}
@Override
public void handleSearchResult(Search search, SearchResult searchResult) {
}
@Override
public void handleSearchResults(Search search, Collection<? extends SearchResult> searchResults) {
}
void killBusy() {
if (busyTimer != null && busyTimer.isRunning()) {
busyTimer.stop();
}
putValue(TabActionMap.BUSY_KEY, Boolean.FALSE);
}
@Override
public void searchStarted(Search search) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
busyTimer = new Timer(60000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
putValue(TabActionMap.BUSY_KEY, Boolean.FALSE);
}
});
busyTimer.setRepeats(false);
busyTimer.start();
putValue(TabActionMap.BUSY_KEY, Boolean.TRUE);
}
});
}
@Override
public void searchStopped(Search search) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
killBusy();
}
});
}
}
private static abstract class SearchListenerAction extends AbstractAction implements SearchListener{
protected final Search search;
public SearchListenerAction(String tr, Search search) {
super(tr);
this.search = search;
}
/**
* Registers the SearchListenerAction as a SearchListener on search.
*
* @return this
*/
public SearchListenerAction register(){
search.addSearchListener(this);
return this;
}
@Override
public void handleSearchResult(Search search, SearchResult searchResult) {}
@Override
public void handleSearchResults(Search search, Collection<? extends SearchResult> searchResults) {}
}
/**
* Action which repeats a search. Since it makes no sense to
* repeat an unstarted search, this class only enables searches to be
* repeated if they have already started.
*/
private static class MoreResultsAction extends SearchListenerAction {
MoreResultsAction(Search search) {
super(I18n.tr("Find More Results"), search);
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent event) {
search.repeat();
}
@Override
public void searchStarted(Search search) {
setEnabled(true);
}
@Override public void searchStopped(Search search) { }
}
/**
* Clears the current search results and repeats the search
*/
private class RepeatSearchAction extends SearchListenerAction {
private SearchResultsModel model;
RepeatSearchAction(String title, Search search, SearchResultsModel model) {
super(title, search);
this.model = model;
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent e) {
if(model.getSearchType() == SearchType.ALL_FRIENDS_BROWSE){
allFriendsRefreshManager.refresh();
} else {
new DefaultSearchRepeater(search, model).refresh();
}
}
@Override
public void searchStopped(Search search) {
}
@Override
public void searchStarted(Search search) {
setEnabled(true);
}
}
/**Stops the search*/
private static class StopSearchAction extends SearchListenerAction {
StopSearchAction(Search search) {
super(I18n.tr("Stop Search"), search);
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent e) {
search.stop();
}
@Override
public void searchStarted(Search search) {
setEnabled(true);
}
@Override
public void searchStopped(Search search) {
setEnabled(false);
}
}
}