package org.limewire.ui.swing.search;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Insets;
import java.awt.Rectangle;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.table.JTableHeader;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.swingx.JXPanel;
import org.jdesktop.swingx.painter.RectanglePainter;
import org.limewire.core.api.search.SearchCategory;
import org.limewire.core.api.search.browse.BrowseStatus;
import org.limewire.setting.evt.SettingEvent;
import org.limewire.setting.evt.SettingListener;
import org.limewire.ui.swing.components.Disposable;
import org.limewire.ui.swing.components.HeaderBar;
import org.limewire.ui.swing.components.decorators.HeaderBarDecorator;
import org.limewire.ui.swing.filter.AdvancedFilterPanel;
import org.limewire.ui.swing.filter.AdvancedFilterPanelFactory;
import org.limewire.ui.swing.filter.AdvancedFilterPanel.CategoryListener;
import org.limewire.ui.swing.friends.refresh.AllFriendsRefreshManager;
import org.limewire.ui.swing.search.SearchResultsMessagePanel.MessageType;
import org.limewire.ui.swing.search.model.SearchResultsModel;
import org.limewire.ui.swing.search.model.VisualSearchResult;
import org.limewire.ui.swing.search.resultpanel.BaseResultPanel.ListViewTable;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.ui.swing.table.TableCellHeaderRenderer;
import org.limewire.ui.swing.util.CategoryIconManager;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
/**
* This is the top-level container for the search results display.
* SearchResultsPanel contains several UI components, including the category
* tab items, sort and filter panel, sponsored results panel, and search
* results tables.
*/
public class SearchResultsPanel extends JXPanel implements Disposable {
/**
* The type of overlay which should be placed over the search results.
* NONE indicates that no overlay should be shown.
* AWAITING_CONNECTIONS indicates that an overlay with a busy icon and a "LimeWire is connecting..." message should be shown.
* NO_FRIENDS_ON_LIMEWIRE indicates that friends' files cannot be shown b/c no friends are logged on
*/
public enum OverlayType {
NONE,
AWAITING_CONNECTIONS,
NO_FRIENDS_ON_LIMEWIRE
}
/** Decorator used to set the appearance of the header bar. */
private final HeaderBarDecorator headerBarDecorator;
/** Icon manager for categories. */
private final CategoryIconManager categoryIconManager;
/** Label that displays the search title. */
private final JLabel searchTitleLabel = new JLabel();
/** Panel containing filter components. */
private final AdvancedFilterPanel<VisualSearchResult> filterPanel;
/**
* This is the subpanel that displays the actual search results.
*/
private final ResultsContainer resultsContainer;
/**
* This is the subpanel that appears in the upper-right corner
* of each search results tab.
*/
private final SortAndFilterPanel sortAndFilterPanel;
/** The scroll pane embedding the search results & sponsored results. */
private JScrollPane scrollPane;
/** The ScrollablePanel that the scroll pane is embedding. */
private ScrollablePanel scrollablePanel;
/** The panel for showing messages and a pointer to the classic search results view button. */
private final SearchResultsMessagePanel messagePanel;
/** This is the white gap which appears between the message panel and the search results */
private Component messagePanelsGap;
/** Listener for changes in the view type. */
private final SettingListener viewTypeListener;
/** Listener for updates to the result count. */
private final ListEventListener<VisualSearchResult> resultCountListener;
/** Search results data model. */
private final SearchResultsModel searchResultsModel;
@Resource private Color tabHighlightTopGradientColor;
@Resource private Color tabHighlightBottomGradientColor;
private boolean lifeCycleComplete = true;
private boolean fullyConnected = true;
private boolean receivedSponsoredResults = false;
private boolean receivedSearchResults = false;
private BrowseStatus browseStatus = null;
/** Shows status of failed browses and refresh button.
*/
private BrowseStatusPanel browseStatusPanel;
/** Title when browsing friends; null for search results. */
private String browseTitle;
private final BrowseFailedMessagePanel browseFailedPanel;
/** This class has a JXLayer as its sole component. The JXLayer has the search results components
as its main panel and sometimes places overlays over these components. */
private final JXLayer jxlayer;
/** This is the active OverlayType for the JXLayer */
private OverlayType overlayType = OverlayType.NONE;
private SettingListener messagePanelGapHider;
/**
* Constructs a SearchResultsPanel with the specified components.
*/
@Inject
public SearchResultsPanel(
@Assisted SearchResultsModel searchResultsModel,
ResultsContainerFactory containerFactory,
SortAndFilterPanelFactory sortAndFilterFactory,
AdvancedFilterPanelFactory<VisualSearchResult> filterPanelFactory,
HeaderBarDecorator headerBarDecorator,
CategoryIconManager categoryIconManager,
BrowseFailedMessagePanelFactory browseFailedMessagePanelFactory,
AllFriendsRefreshManager allFriendsRefreshManager) {
super(new BorderLayout());
GuiUtils.assignResources(this);
this.searchResultsModel = searchResultsModel;
this.headerBarDecorator = headerBarDecorator;
this.categoryIconManager = categoryIconManager;
this.browseFailedPanel = browseFailedMessagePanelFactory.create(searchResultsModel);
// Create sort and filter components.
sortAndFilterPanel = sortAndFilterFactory.create(searchResultsModel);
filterPanel = filterPanelFactory.create(searchResultsModel, searchResultsModel.getSearchType());
scrollPane = new JScrollPane();
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollablePanel = new ScrollablePanel();
configureEnclosingScrollPane();
// Create results container with tables.
resultsContainer = containerFactory.create(searchResultsModel);
viewTypeListener = new SettingListener() {
int oldSearchViewTypeId = SwingUiSettings.SEARCH_VIEW_TYPE_ID.getValue();
@Override
public void settingChanged(SettingEvent evt) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
int newSearchViewTypeId = SwingUiSettings.SEARCH_VIEW_TYPE_ID.getValue();
if(newSearchViewTypeId != oldSearchViewTypeId) {
SearchViewType newSearchViewType = SearchViewType.forId(newSearchViewTypeId);
resultsContainer.setViewType(newSearchViewType);
syncScrollPieces();
oldSearchViewTypeId = newSearchViewTypeId;
}
}
});
}
};
SwingUiSettings.SEARCH_VIEW_TYPE_ID.addSettingListener(viewTypeListener);
// Initialize header label.
updateTitle();
// Install listener to update header label.
resultCountListener = new ListEventListener<VisualSearchResult>() {
@Override
public void listChanged(ListEvent<VisualSearchResult> listChanges) {
// these updates are coming in on the AWT thread. so, it's safe to make GUI updates here.
updateTitle();
receivedSearchResults = (SearchResultsPanel.this.searchResultsModel.getUnfilteredList().size() > 0);
updateMessages();
}
};
searchResultsModel.getUnfilteredList().addListEventListener(resultCountListener);
searchResultsModel.getFilteredList().addListEventListener(resultCountListener);
// Configure sort panel and results container.
sortAndFilterPanel.setSearchCategory(searchResultsModel.getSearchCategory());
resultsContainer.showCategory(searchResultsModel.getSearchCategory());
syncScrollPieces();
// Configure advanced filters.
filterPanel.setSearchCategory(searchResultsModel.getSearchCategory());
filterPanel.addCategoryListener(new CategoryListener() {
@Override
public void categorySelected(SearchCategory displayCategory) {
sortAndFilterPanel.setSearchCategory(displayCategory);
resultsContainer.showCategory(displayCategory);
syncScrollPieces();
updateTitle();
}
});
browseStatusPanel = new BrowseStatusPanel(searchResultsModel, allFriendsRefreshManager);
messagePanel = new SearchResultsMessagePanel();
// if the user closes the hint showing where the classic search view is by clicking on the hint's close button,
// we need to hear about that in this class so that we can make the gap separating the message box from
// the search results disappear
if (messagePanel.isShowClassicSearchResultsHint()) {
messagePanelGapHider = new SettingListener() {
@Override
public void settingChanged(SettingEvent evt) {
if (!SwingUiSettings.SHOW_CLASSIC_REMINDER.getValue()) {
messagePanelsGap.setVisible(false);
}
}
};
SwingUiSettings.SHOW_CLASSIC_REMINDER.addSettingListener(messagePanelGapHider);
}
jxlayer = new JXLayer<JComponent>(createSearchResultsPanel());
jxlayer.getGlassPane().setLayout(new BorderLayout());
add(jxlayer, BorderLayout.CENTER);
}
/**
* This method controls whether an overlay should be shown and if so, which type of overlay.
*
* @param overlayType -- the overlay type. See the type definition for more info.
*/
void setOverlayType(OverlayType overlayType) {
// this method can be called multiple times for the same overlay type.
// so, let's check to make sure that the overlay actually needs to change
// before installing or uninstalling an overlay panel.
if (this.overlayType != overlayType) {
this.overlayType = overlayType;
switch (overlayType) {
case AWAITING_CONNECTIONS:
installOverlay(new AwaitingConnectionsPanel(headerBarDecorator));
break;
case NO_FRIENDS_ON_LIMEWIRE:
installOverlay(browseFailedPanel);
break;
case NONE:
uninstallOverlay();
break;
default:
throw new IllegalStateException("invalid type: " + overlayType);
}
}
}
private void installOverlay(JComponent component) {
jxlayer.getGlassPane().setVisible(false);
jxlayer.getGlassPane().removeAll();
jxlayer.getGlassPane().add(component);
jxlayer.getGlassPane().setVisible(true);
}
private void uninstallOverlay() {
jxlayer.getGlassPane().setVisible(false);
jxlayer.getGlassPane().removeAll();
}
/**
* Disposes of resources used by the container. This method is called when
* the search is closed.
*/
@Override
public void dispose() {
SwingUiSettings.SEARCH_VIEW_TYPE_ID.removeSettingListener(viewTypeListener);
if (messagePanelGapHider != null) SwingUiSettings.SHOW_CLASSIC_REMINDER.removeSettingListener(messagePanelGapHider);
searchResultsModel.getFilteredList().removeListEventListener(resultCountListener);
searchResultsModel.getUnfilteredList().removeListEventListener(resultCountListener);
sortAndFilterPanel.dispose();
filterPanel.dispose();
messagePanel.dispose();
browseFailedPanel.dispose();
searchResultsModel.dispose();
browseStatusPanel.dispose();
}
/**
* @return the SearchResultsModel of the SearchResultsPanel.
*/
public SearchResultsModel getModel(){
return searchResultsModel;
}
/**
* Fills in the top right corner if a scrollbar appears with an empty table
* header.
*/
protected void configureEnclosingScrollPane() {
JTableHeader th = new JTableHeader();
th.setDefaultRenderer(new TableCellHeaderRenderer());
// Put a dummy header in the upper-right corner.
final Component renderer = th.getDefaultRenderer().getTableCellRendererComponent(null, "", false, false, -1, -1);
JPanel cornerComponent = new JPanel(new BorderLayout());
cornerComponent.add(renderer, BorderLayout.CENTER);
scrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER, cornerComponent);
}
/**
* Sets the browse title in the container. When not null, the browse title
* is displayed at the top of the panel. When null, the container displays
* the search title from the data model.
*/
public void setBrowseTitle(String title) {
browseTitle = title;
updateTitle();
}
/**
* Updates the title icon and text in the container. For search results,
* the title includes the category name, search title, and result counts.
*/
private void updateTitle() {
// Get result counts.
int total = searchResultsModel.getUnfilteredList().size();
int actual = searchResultsModel.getFilteredList().size();
if (browseTitle != null) {
// Set browse title.
searchTitleLabel.setText((actual == total) ?
// {0}: browse title, {1}: total count
I18n.tr("Browse {0} ({1})", browseTitle, total) :
// {0}: browse title, {1}: actual count, {2}: total count
I18n.tr("Browse {0} - Showing {1} of {2}", browseTitle, actual, total));
} else {
// Get search category and title.
SearchCategory displayCategory = searchResultsModel.getSelectedCategory();
String title = searchResultsModel.getSearchTitle();
// Set title icon based on category.
Icon icon = (displayCategory == SearchCategory.ALL) ? null :
categoryIconManager.getIcon(displayCategory.getCategory());
searchTitleLabel.setIcon(icon);
// Set title text.
switch (displayCategory) {
case ALL:
searchTitleLabel.setText((actual == total) ?
// {0}: search title, {1}: total count
I18n.tr("All results for {0} ({1})", title, total) :
// {0}: search title, {1}: actual count, {2}: total count
I18n.tr("All results for {0} - Showing {1} of {2}", title, actual, total));
break;
case AUDIO:
searchTitleLabel.setText((actual == total) ?
// {0}: search title, {1}: total count
I18n.tr("Audio results for {0} ({1})", title, total) :
// {0}: search title, {1}: actual count, {2}: total count
I18n.tr("Audio results for {0} - Showing {1} of {2}", title, actual, total));
break;
case VIDEO:
searchTitleLabel.setText((actual == total) ?
// {0}: search title, {1}: total count
I18n.tr("Video results for {0} ({1})", title, total) :
// {0}: search title, {1}: actual count, {2}: total count
I18n.tr("Video results for {0} - Showing {1} of {2}", title, actual, total));
break;
case IMAGE:
searchTitleLabel.setText((actual == total) ?
// {0}: search title, {1}: total count
I18n.tr("Image results for {0} ({1})", title, total) :
// {0}: search title, {1}: actual count, {2}: total count
I18n.tr("Image results for {0} - Showing {1} of {2}", title, actual, total));
break;
case DOCUMENT:
searchTitleLabel.setText((actual == total) ?
// {0}: search title, {1}: total count
I18n.tr("Document results for {0} ({1})", title, total) :
// {0}: search title, {1}: actual count, {2}: total count
I18n.tr("Document results for {0} - Showing {1} of {2}", title, actual, total));
break;
case PROGRAM:
searchTitleLabel.setText((actual == total) ?
// {0}: search title, {1}: total count
I18n.tr("Program results for {0} ({1})", title, total) :
// {0}: search title, {1}: actual count, {2}: total count
I18n.tr("Program results for {0} - Showing {1} of {2}", title, actual, total));
break;
case OTHER:
searchTitleLabel.setText((actual == total) ?
// {0}: search title, {1}: total count
I18n.tr("Other results for {0} ({1})", title, total) :
// {0}: search title, {1}: actual count, {2}: total count
I18n.tr("Other results for {0} - Showing {1} of {2}", title, actual, total));
break;
case TORRENT:
searchTitleLabel.setText((actual == total) ?
// {0}: search title, {1}: total count
I18n.tr("Torrent results for {0} ({1})", title, total) :
// {0}: search title, {1}: actual count, {2}: total count
I18n.tr("Torrent results for {0} - Showing {1} of {2}", title, actual, total));
break;
default:
throw new IllegalStateException("Invalid search category " + displayCategory);
}
}
}
/**
* Updates the column header component in the scroll pane. This depends on
* the current results view and whether the sponsored results are visible.
*/
private void syncColumnHeader() {
Component resultHeader = resultsContainer.getScrollPaneHeader();
scrollPane.setColumnHeaderView(resultHeader);
scrollPane.validate();
// Resize and repaint table header. This eliminates visual issues due
// to a change in the table format, which can result in an incorrect
// header height or header flickering when a category is selected.
if (resultHeader instanceof JTableHeader) {
((JTableHeader) resultHeader).resizeAndRepaint();
}
}
/**
* Initializes the components and adds them to the container. Called by
* the constructor.
*/
private JPanel createSearchResultsPanel() {
JPanel searchResultsComponentsPanel = new JPanel( new MigLayout("hidemode 2, insets 0 0 0 0, gap 0!, novisualpadding",
"[][grow]", // col constraints
"[][][][grow]") ); // row constraints
searchResultsComponentsPanel.setMinimumSize(new Dimension(searchResultsComponentsPanel.getPreferredSize().width, 33));
RectanglePainter tabHighlight = new RectanglePainter();
tabHighlight.setFillPaint(new GradientPaint(20.0f, 0.0f, tabHighlightTopGradientColor,
20.0f, 33.0f, tabHighlightBottomGradientColor));
tabHighlight.setInsets(new Insets(0,0,1,0));
tabHighlight.setBorderPaint(null);
HeaderBar header = new HeaderBar(searchTitleLabel);
header.setLayout(new MigLayout("insets 0, gap 0!, novisualpadding, alignx 100%, aligny 50%"));
header.add(browseStatusPanel, "alignx 0%, growx, pushx");
headerBarDecorator.decorateBasic(header);
sortAndFilterPanel.layoutComponents(header);
searchResultsComponentsPanel.add(header, "spanx 2, growx, growy, wrap");
searchResultsComponentsPanel.add(filterPanel, "grow, spany 3");
searchResultsComponentsPanel.add(messagePanel, "spanx 1, growx, wrap");
messagePanelsGap = Box.createVerticalStrut(6);
searchResultsComponentsPanel.add(messagePanelsGap, "hidemode 3, spanx 1, growx, wrap");
searchResultsComponentsPanel.add(scrollPane , "hidemode 3, grow, spany");
scrollablePanel.setScrollableTracksViewportHeight(false);
scrollablePanel.setLayout(new BorderLayout());
scrollablePanel.add(resultsContainer, BorderLayout.CENTER);
scrollPane.setViewportView(scrollablePanel);
syncScrollPieces();
return searchResultsComponentsPanel;
}
/**
* Updates the view components in the scroll pane.
*/
private void syncScrollPieces() {
scrollablePanel.setScrollable(resultsContainer.getScrollable());
syncColumnHeader();
}
/**
* Panel used as the viewport view in the scroll pane. This contains the
* results table and sponsored results panel in a single, scrollable area.
*/
private class ScrollablePanel extends JXPanel {
private Scrollable scrollable;
public void setScrollable(Scrollable scrollable) {
this.scrollable = scrollable;
}
@Override
public Dimension getPreferredSize() {
if(scrollable == null) {
return super.getPreferredSize();
} else {
int width = super.getPreferredSize().width;
int height = ((JComponent)scrollable).getPreferredSize().height;
// the list view has some weird rendering sometimes (double space after last result)
// so don't fill full screen on list view
if(! (scrollable instanceof ListViewTable)) {
int headerHeight = 0;
//the table headers aren't being set on the scrollpane, so if its visible check its
// height and subtract it from the viewport size
JTableHeader header = ((JTable)scrollable).getTableHeader();
if(header != null && header.isShowing()) {
headerHeight = header.getHeight();
}
// if the height of table is less than the scrollPane height, set preferred height
// to same size as scrollPane
if(height < scrollPane.getSize().height - headerHeight) {
height = scrollPane.getSize().height - headerHeight;
}
}
return new Dimension(width, height);
}
}
@Override
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
if(scrollable == null) {
return super.getScrollableUnitIncrement(visibleRect, orientation, direction);
} else {
return scrollable.getScrollableUnitIncrement(visibleRect, orientation, direction);
}
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
if(scrollable == null) {
return super.getScrollableBlockIncrement(visibleRect, orientation, direction);
} else {
return scrollable.getScrollableBlockIncrement(visibleRect, orientation, direction);
}
}
}
/**
* Sets an indicator to determine whether the application has finished
* loading, and updates the user message.
*/
public void setLifeCycleComplete(boolean lifeCycleComplete) {
this.lifeCycleComplete = lifeCycleComplete;
updateMessages();
}
/**
* Sets an indicator to determine whether the application is fully
* connected to the P2P Network, and updates the user message.
*/
public void setFullyConnected(boolean fullyConnected) {
this.fullyConnected = fullyConnected;
updateMessages();
}
public void setBrowseStatus(BrowseStatus browseStatus){
this.browseStatus = browseStatus;
updateMessages();
}
/**
* Updates the user message based on the current state of the application.
*/
private void updateMessages() {
browseStatusPanel.setBrowseStatus(browseStatus);
// let's check whether we need to show the user any messages
if (!fullyConnected && (receivedSearchResults || receivedSponsoredResults)) {
messagePanel.setMessageType(MessageType.CONNECTING_TO_ULTRAPEERS);
messagePanelsGap.setVisible(true);
} else if (fullyConnected && receivedSearchResults && messagePanel.isShowClassicSearchResultsHint()) {
messagePanel.setMessageType(MessageType.CLASSIC_SEARCH_RESULTS_HINT);
messagePanelsGap.setVisible(true);
} else {
messagePanel.setMessageType(MessageType.NONE);
messagePanelsGap.setVisible(false);
}
// let's check whether we need to put an overlay over the search results panel
if ( (!lifeCycleComplete || !fullyConnected ) && (!receivedSearchResults && !receivedSponsoredResults) ) {
setOverlayType(OverlayType.AWAITING_CONNECTIONS);
} else if (browseStatus != null && !browseStatus.getState().isOK()) {
browseFailedPanel.update(browseStatus.getState(), browseStatus.getBrowseSearch(), browseStatus.getFailedFriends());
setOverlayType(OverlayType.NO_FRIENDS_ON_LIMEWIRE);
} else {
setOverlayType(OverlayType.NONE);
}
}
}