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 java.util.List;
import javax.swing.BorderFactory;
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.DefaultTableColumnModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.jdesktop.swingx.JXPanel;
import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.painter.RectanglePainter;
import org.limewire.core.api.search.SearchCategory;
import org.limewire.core.api.search.browse.BrowseStatus;
import org.limewire.core.api.search.sponsored.SponsoredResult;
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.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 SponsoredResultsView, Disposable {
/** 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 sponsored results. */
private final SponsoredResultsPanel sponsoredResultsPanel;
/** The scroll pane embedding the search results & sponsored results. */
private JScrollPane scrollPane;
/** The ScrollablePanel that the scroll pane is embedding. */
private ScrollablePanel scrollablePanel;
/** The label where text about your search started poorly or later is written. */
private JLabel messageLabel;
/** The panel that displays the {@link #messageLabel}. */
private JPanel messagePanel;
/** The class search warning panel. */
private ClassicSearchWarningPanel classicSearchReminderPanel;
/** 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 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;
/**
* Constructs a SearchResultsPanel with the specified components.
*/
@Inject
public SearchResultsPanel(
@Assisted SearchResultsModel searchResultsModel,
ResultsContainerFactory containerFactory,
SortAndFilterPanelFactory sortAndFilterFactory,
AdvancedFilterPanelFactory<VisualSearchResult> filterPanelFactory,
SponsoredResultsPanel sponsoredResultsPanel,
HeaderBarDecorator headerBarDecorator,
CategoryIconManager categoryIconManager,
BrowseFailedMessagePanelFactory browseFailedMessagePanelFactory,
AllFriendsRefreshManager allFriendsRefreshManager) {
GuiUtils.assignResources(this);
this.searchResultsModel = searchResultsModel;
this.headerBarDecorator = headerBarDecorator;
this.categoryIconManager = categoryIconManager;
this.sponsoredResultsPanel = sponsoredResultsPanel;
this.sponsoredResultsPanel.setVisible(false);
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) {
updateTitle();
}
};
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();
}
});
messageLabel = new JLabel();
messagePanel = new JPanel();
messagePanel.add(messageLabel);
messagePanel.setVisible(false);
browseStatusPanel = new BrowseStatusPanel(searchResultsModel, allFriendsRefreshManager);
classicSearchReminderPanel = new ClassicSearchWarningPanel();
layoutComponents();
}
/**
* 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);
searchResultsModel.getFilteredList().removeListEventListener(resultCountListener);
searchResultsModel.getUnfilteredList().removeListEventListener(resultCountListener);
sortAndFilterPanel.dispose();
filterPanel.dispose();
classicSearchReminderPanel.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);
}
/**
* Adds the specified list of sponsored results to the display.
*/
@Override
public void addSponsoredResults(List<SponsoredResult> sponsoredResults){
for (SponsoredResult result : sponsoredResults){
sponsoredResultsPanel.addEntry(result);
}
if (!sponsoredResultsPanel.isVisible()) {
sponsoredResultsPanel.setVisible(true);
syncColumnHeader();
}
}
/**
* 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;
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();
if (resultHeader == null) {
// If no headers, use nothing special.
scrollPane.setColumnHeaderView(null);
} else if (!sponsoredResultsPanel.isVisible()) {
// If sponsored results aren't visible, just use the actual header.
scrollPane.setColumnHeaderView(resultHeader);
} else {
// Otherwise, create a combined panel that has both sponsored results & header.
JXPanel headerPanel = new JXPanel();
// Make sure this syncs with the layout for the results & sponsored results!
headerPanel.setLayout(new MigLayout("hidemode 3, gap 0, insets 0", "[]", "[grow][]"));
headerPanel.add(resultHeader, "grow, push, alignx left, aligny top");
DefaultTableColumnModel model = new DefaultTableColumnModel();
TableColumn column = new TableColumn();
model.addColumn(column);
JTableHeader header = new JTableHeader(model);
header.setDefaultRenderer(new TableCellHeaderRenderer());
header.setReorderingAllowed(false);
header.setResizingAllowed(false);
header.setTable(new JXTable(0, 1));
int width = sponsoredResultsPanel.getPreferredSize().width;
int height = resultHeader.getPreferredSize().height;
column.setWidth(width);
Dimension dimension = new Dimension(width, height);
header.setPreferredSize(dimension);
header.setMaximumSize(dimension);
header.setMinimumSize(dimension);
headerPanel.add(header, "aligny top, alignx right");
scrollPane.setColumnHeaderView(headerPanel);
}
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 void layoutComponents() {
MigLayout layout = new MigLayout("hidemode 2, insets 0 0 0 0, gap 0!, novisualpadding",
"[][grow]", // col constraints
"[][][][grow]"); // row constraints
setLayout(layout);
setMinimumSize(new Dimension(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);
add(header , "spanx 2, growx, growy, wrap");
add(classicSearchReminderPanel, "spanx 2, growx, wrap");
add(messagePanel , "spanx 2, growx, wrap");
add(filterPanel, "grow");
add(scrollPane , "hidemode 3, grow");
add(browseFailedPanel , "hidemode 3, grow");
scrollablePanel.setScrollableTracksViewportHeight(false);
scrollablePanel.setLayout(new BorderLayout());
scrollablePanel.add(resultsContainer, BorderLayout.CENTER);
scrollablePanel.add(sponsoredResultsPanel, BorderLayout.EAST);
scrollPane.setViewportView(scrollablePanel);
syncScrollPieces();
}
/**
* 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)) {
// old check, if sponsored results aren't showing properlly revert to just using this
if(sponsoredResultsPanel.isVisible()) {
height = Math.max(height, sponsoredResultsPanel.getPreferredSize().height);
}
} else { // classic view
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);
if (!lifeCycleComplete) {
messageLabel.setText(I18n.tr("LimeWire will start your search right after it finishes loading."));
messagePanel.setVisible(true);
browseFailedPanel.setVisible(false);
} else if (!fullyConnected) {
messageLabel.setText(I18n.tr("You might not receive many results until LimeWire finishes loading..."));
messagePanel.setVisible(true);
browseFailedPanel.setVisible(false);
} else if (browseStatus != null && !browseStatus.getState().isOK()) {
browseFailedPanel.update(browseStatus.getState(), browseStatus.getBrowseSearch(), browseStatus.getFailedFriends());
browseFailedPanel.setVisible(true);
} else {
messagePanel.setVisible(false);
browseFailedPanel.setVisible(false);
}
filterPanel.setVisible(!browseFailedPanel.isVisible());
scrollPane.setVisible(!browseFailedPanel.isVisible());
}
}