package org.limewire.ui.swing.search.resultpanel; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Graphics; import java.util.Calendar; import java.util.HashMap; import java.util.Map; import javax.swing.JLabel; import javax.swing.Scrollable; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import org.jdesktop.application.Resource; import org.jdesktop.swingx.JXPanel; import org.jdesktop.swingx.decorator.ColorHighlighter; import org.jdesktop.swingx.decorator.ComponentAdapter; import org.jdesktop.swingx.decorator.HighlightPredicate; import org.jdesktop.swingx.table.TableColumnExt; import org.limewire.collection.glazedlists.GlazedListsFactory; import org.limewire.core.api.search.SearchCategory; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.ui.swing.components.Disposable; import org.limewire.ui.swing.components.DisposalListener; import org.limewire.ui.swing.components.RemoteHostWidgetFactory; import org.limewire.ui.swing.components.RemoteHostWidget.RemoteWidgetType; import org.limewire.ui.swing.downloads.DownloadMediator; import org.limewire.ui.swing.library.LibraryMediator; import org.limewire.ui.swing.search.SearchViewType; import org.limewire.ui.swing.search.model.SearchResultsModel; import org.limewire.ui.swing.search.model.VisualSearchResult; import org.limewire.ui.swing.search.resultpanel.classic.AllTableFormat; import org.limewire.ui.swing.search.resultpanel.classic.AudioTableFormat; import org.limewire.ui.swing.search.resultpanel.classic.ClassicDoubleClickHandler; import org.limewire.ui.swing.search.resultpanel.classic.DocumentTableFormat; import org.limewire.ui.swing.search.resultpanel.classic.FromTableCellRenderer; import org.limewire.ui.swing.search.resultpanel.classic.ImageTableFormat; import org.limewire.ui.swing.search.resultpanel.classic.OtherTableFormat; import org.limewire.ui.swing.search.resultpanel.classic.ProgramTableFormat; import org.limewire.ui.swing.search.resultpanel.classic.ResultEnterAction; import org.limewire.ui.swing.search.resultpanel.classic.TorrentTableFormat; import org.limewire.ui.swing.search.resultpanel.classic.VideoTableFormat; import org.limewire.ui.swing.search.resultpanel.list.ListViewDisplayedRowsLimit; import org.limewire.ui.swing.search.resultpanel.list.ListViewRowHeightRule; import org.limewire.ui.swing.search.resultpanel.list.ListViewTableEditorRenderer; import org.limewire.ui.swing.search.resultpanel.list.ListViewTableEditorRendererFactory; import org.limewire.ui.swing.search.resultpanel.list.ListViewTableFormat; import org.limewire.ui.swing.search.resultpanel.list.ListViewRowHeightRule.RowDisplayResult; import org.limewire.ui.swing.table.CalendarRenderer; import org.limewire.ui.swing.table.DefaultLimeTableCellRenderer; import org.limewire.ui.swing.table.FileSizeRenderer; import org.limewire.ui.swing.table.IconLabelRendererFactory; import org.limewire.ui.swing.table.MultilineTooltipRenderer; import org.limewire.ui.swing.table.QualityRenderer; import org.limewire.ui.swing.table.TableCellHeaderRenderer; import org.limewire.ui.swing.table.TableColors; import org.limewire.ui.swing.table.TimeRenderer; import org.limewire.ui.swing.util.EventListJXTableSorting; import org.limewire.ui.swing.util.GuiUtils; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.ListSelection; import ca.odell.glazedlists.RangeList; import ca.odell.glazedlists.SortedList; import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEventListener; import ca.odell.glazedlists.swing.DefaultEventSelectionModel; import ca.odell.glazedlists.swing.DefaultEventTableModel; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; /** * Base class containing the search results tables for a single category. * BaseResultPanel contains both the List view and Table view components. The * current view is selected by calling the <code>setViewType()</code> method. * the display category is selected by calling the <code>showCategory()</code> * method. */ public class BaseResultPanel extends JXPanel { private static final int MAX_DISPLAYED_RESULT_SIZE = 500; private static final int TABLE_ROW_HEIGHT = 23; private static final int ROW_HEIGHT = ListViewRowHeightRule.RowDisplayConfig.HeadingAndMetadata.getRowHeight(); private final ListViewTableEditorRendererFactory listViewTableEditorRendererFactory; private static final Log LOG = LogFactory.getLog(BaseResultPanel.class); /** Table component for the List view. */ private final ListViewTable resultsList; /** The category the list is currently configured for. */ private SearchCategory listConfiguredFor; /** Table component for the Table view. */ private final ResultsTable<VisualSearchResult> resultsTable; /** The category the table is currently configured for. */ private SearchCategory tableConfiguredFor; /** The currently filtered SearchCategory. */ private SearchCategory currentCategory; /** cache for RowDisplayResult which could be expensive to generate with large search result sets */ private final Map<VisualSearchResult, RowDisplayResult> vsrToRowDisplayResultMap = new HashMap<VisualSearchResult, RowDisplayResult>(); /** Data model containing search results. */ private final SearchResultsModel searchResultsModel; private final ResultsTableFormatFactory tableFormatFactory; private final ListViewRowHeightRule rowHeightRule; private final RemoteHostWidgetFactory fromWidgetfactory; private final Provider<IconLabelRendererFactory> iconLabelRendererFactory; private final DownloadHandler downloadHandler; private final Provider<TimeRenderer> timeRenderer; private final Provider<FileSizeRenderer> fileSizeRenderer; private final Provider<CalendarRenderer> calendarRenderer; private final Provider<QualityRenderer> qualityRenderer; private final DefaultLimeTableCellRenderer defaultTableCellRenderer; private RangeList<VisualSearchResult> maxSizedList; private ListEventListener<VisualSearchResult> maxSizedListener; private DefaultEventSelectionModel<VisualSearchResult> listSelectionModel; private EventListJXTableSorting resultsTableSorting; private DefaultEventSelectionModel<VisualSearchResult> selectionModel; private ColorHighlighter resultsColorHighlighter; private Scrollable visibleComponent; private final SearchResultMenuFactory menuFactory; /** * Constructs a BaseResultPanel with the specified components. */ @Inject public BaseResultPanel( @Assisted SearchResultsModel searchResultsModel, ResultsTableFormatFactory tableFormatFactory, ListViewTableEditorRendererFactory listViewTableEditorRendererFactory, ListViewRowHeightRule rowHeightRule, RemoteHostWidgetFactory fromWidgetFactory, SearchResultMenuFactory menuFactory, Provider<IconLabelRendererFactory> iconLabelRendererFactory, Provider<TimeRenderer> timeRenderer, Provider<FileSizeRenderer> fileSizeRenderer, Provider<CalendarRenderer> calendarRenderer, LibraryMediator libraryMediator, Provider<QualityRenderer> qualityRenderer, DefaultLimeTableCellRenderer defaultTableCellRenderer, DownloadMediator downloadMediator) { this.searchResultsModel = searchResultsModel; this.tableFormatFactory = tableFormatFactory; this.listViewTableEditorRendererFactory = listViewTableEditorRendererFactory; this.rowHeightRule = rowHeightRule; this.fromWidgetfactory = fromWidgetFactory; this.iconLabelRendererFactory = iconLabelRendererFactory; this.downloadHandler = new DownloadHandlerImpl(searchResultsModel, libraryMediator, downloadMediator); this.timeRenderer = timeRenderer; this.fileSizeRenderer = fileSizeRenderer; this.calendarRenderer = calendarRenderer; this.qualityRenderer = qualityRenderer; this.defaultTableCellRenderer = defaultTableCellRenderer; this.menuFactory = menuFactory; rowHeightRule.initializeWithSearch(searchResultsModel.getSearchQuery()); // Create tables. this.resultsList = createList(); this.resultsTable = createTable(); searchResultsModel.addDisposalListener(new ResultModelDisposalListener()); setLayout(new BorderLayout()); } /** * Creates a new List view table. */ private ListViewTable createList() { ListViewTable listTable = new ListViewTable(); // Set list table fields that do not change with search category. listTable.setShowGrid(true, false); listTable.setRowHeightEnabled(true); listTable.setEmptyRowsPainted(false); return listTable; } /** * Creates a new Table view table. */ private ResultsTable<VisualSearchResult> createTable() { ResultsTable<VisualSearchResult> table = new ResultsTable<VisualSearchResult>(); // Set table fields that do not change with search category. table.setPopupHandler(new SearchPopupHandler(downloadHandler, table, menuFactory)); table.setDoubleClickHandler(new ClassicDoubleClickHandler(table, downloadHandler)); table.setRowHeight(TABLE_ROW_HEIGHT); return table; } /** * Configures the List view to display results for the selected category. */ private void configureList() { LOG.debugf("Configuring list view for {0}, configured already for {1}", currentCategory, listConfiguredFor); listConfiguredFor = currentCategory; // Remove listener with reference to previous list. if (maxSizedList != null) { maxSizedList.removeListEventListener(maxSizedListener); } // Get sorted list for selected category. final EventList<VisualSearchResult> sortedList = searchResultsModel.getSortedSearchResults(); // Create sized list. (the old one will be disposed when we set the new one on the model) maxSizedList = GlazedListsFactory.rangeList(sortedList); maxSizedList.setHeadRange(0, MAX_DISPLAYED_RESULT_SIZE + 1); // Create table format and set table model. ListViewTableFormat tableFormat = new ListViewTableFormat(); resultsList.setEventListFormat(maxSizedList, tableFormat, false); // Create selection model, and set Enter key action. if (listSelectionModel != null) { listSelectionModel.dispose(); } listSelectionModel = new DefaultEventSelectionModel<VisualSearchResult>(maxSizedList); listSelectionModel.setSelectionMode(ListSelection.MULTIPLE_INTERVAL_SELECTION_DEFENSIVE); resultsList.setSelectionModel(listSelectionModel); resultsList.setEnterKeyAction(new ResultEnterAction(listSelectionModel.getSelected(), downloadHandler)); // Represents display limits for displaying search results in list view. // The limits are introduced to avoid a performance penalty caused by // very large (> 1k) search results. Variable row-height in the list // view is calculated by looping through all results in the table // and if the table holds many results, the performance penalty of // resizing all rows is noticeable past a certain number of rows. ListViewDisplayedRowsLimit displayLimit = new ListViewDisplayedRowsLimit() { @Override public int getLastDisplayedRow() { return MAX_DISPLAYED_RESULT_SIZE; } @Override public int getTotalResultsReturned() { return sortedList.size(); } }; // Note that the same ListViewTableCellEditor instance // cannot be used for both the editor and the renderer // because the renderer receives paint requests for some cells // while another cell is being edited // and they can't share state (the list of sources). // The two ListViewTableCellEditor instances // can share the same ActionColumnTableCellEditor though. ListViewTableEditorRenderer renderer = listViewTableEditorRendererFactory.create( downloadHandler, rowHeightRule, displayLimit); ListViewTableEditorRenderer editor = listViewTableEditorRendererFactory.create( downloadHandler, rowHeightRule, displayLimit); TableColumnModel tcm = resultsList.getColumnModel(); int columnCount = tableFormat.getColumnCount(); for (int i = 0; i < columnCount; i++) { TableColumn tc = tcm.getColumn(i); tc.setCellRenderer(renderer); tc.setCellEditor(editor); } resultsList.setDefaultEditor(VisualSearchResult.class, editor); // Set default width of all visible columns. for (int i = 0; i < tableFormat.getColumnCount(); i++) { resultsList.getColumnModel().getColumn(i).setPreferredWidth(tableFormat.getInitialWidth(i)); } //add listener to table model to set row heights based on contents of the search results maxSizedListener = new ListEventListener<VisualSearchResult>() { @Override public void listChanged(ListEvent<VisualSearchResult> listChanges) { DefaultEventTableModel tableModel = resultsList.getEventTableModel(); if (tableModel.getRowCount() == 0) { return; } //Push row resizing to the end of the event dispatch queue Runnable runner = new Runnable() { @Override public void run() { DefaultEventTableModel model = resultsList.getEventTableModel(); resultsList.setIgnoreRepaints(true); boolean setRowSize = false; for(int row = 0; row < model.getRowCount(); row++) { VisualSearchResult vsr = (VisualSearchResult) model.getElementAt(row); RowDisplayResult result = vsrToRowDisplayResultMap.get(vsr); if (result == null || result.isStale(vsr)) { result = rowHeightRule.getDisplayResult(vsr); vsrToRowDisplayResultMap.put(vsr, result); } int newRowHeight = result.getConfig().getRowHeight(); if(vsr.getSimilarityParent() == null) { //only resize rows that belong to parent visual results. //this will prevent the jumping when expanding child results as mentioned in //https://www.limewire.org/jira/browse/LWC-2545 if (resultsList.getRowHeight(row) != newRowHeight) { resultsList.setRowHeight(row, newRowHeight); setRowSize = true; } } } resultsList.setIgnoreRepaints(false); if (setRowSize) { if (resultsList.isEditing()) { resultsList.editingCanceled(new ChangeEvent(resultsList)); } resultsList.updateViewSizeSequence(); resultsList.resizeAndRepaint(); } } }; SwingUtilities.invokeLater(runner); } }; maxSizedList.addListEventListener(maxSizedListener); resultsList.setRowHeight(ROW_HEIGHT); } /** * Configures the Table view to display results for the selected category. */ private void configureTable() { LOG.debugf("Configuring table view for {0}, configured already for {1}", currentCategory, tableConfiguredFor); tableConfiguredFor = currentCategory; // Uninstall components with references to previous list. if (resultsTableSorting != null) { resultsTableSorting.uninstall(); } if (resultsColorHighlighter != null) { resultsTable.removeHighlighter(resultsColorHighlighter); } // Get results list and table format for selected category. SearchCategory selectedCategory = searchResultsModel.getSelectedCategory(); EventList<VisualSearchResult> eventList = searchResultsModel.getFilteredSearchResults(); ResultsTableFormat<VisualSearchResult> tableFormat = tableFormatFactory.createTableFormat(selectedCategory); // Create sorted list and set table model. SortedList<VisualSearchResult> sortedList = GlazedListsFactory.sortedList(eventList, null); EventList<VisualSearchResult> downstreamList = sortedList; resultsTable.setEventListFormat(downstreamList, tableFormat, true); //link the jxtable column headers to the sorted list resultsTableSorting = EventListJXTableSorting.install(resultsTable, sortedList, tableFormat); //create and install new EventSelectionModel and enter key action if (selectionModel != null) { selectionModel.dispose(); } selectionModel = new DefaultEventSelectionModel<VisualSearchResult>(downstreamList); resultsTable.setSelectionModel(selectionModel); resultsTable.setEnterKeyAction(new ResultEnterAction(selectionModel.getSelected(), downloadHandler)); setupCellRenderers(tableFormat); // Apply column settings for table format. resultsTable.applySavedColumnSettings(); TableColors tableColors = new TableColors(); resultsColorHighlighter = new ColorHighlighter(new DownloadedHighlightPredicate(downstreamList), null, tableColors.getDisabledForegroundColor(), null, tableColors.getDisabledForegroundColor()); resultsTable.addHighlighter(resultsColorHighlighter); } /** * Initializes cell renderers in the Table view column model based on * column types provided by the specified table format. */ protected void setupCellRenderers(ResultsTableFormat<VisualSearchResult> tableFormat) { SearchCategory selectedCategory = searchResultsModel.getSelectedCategory(); TableCellRenderer nameRenderer = iconLabelRendererFactory.get().createIconRenderer(selectedCategory == SearchCategory.ALL); int columnCount = tableFormat.getColumnCount(); for (int i = 0; i < columnCount; i++) { Class clazz = tableFormat.getColumnClass(i); if (clazz == String.class || clazz == Integer.class || clazz == Long.class) { setCellRenderer(i, defaultTableCellRenderer); setCellEditor(i, null); } else if (clazz == Calendar.class) { setCellRenderer(i, calendarRenderer.get()); setCellEditor(i, null); } else if (i == tableFormat.getNameColumn()) { setCellRenderer(i, nameRenderer); setCellEditor(i, null); } else if (VisualSearchResult.class.isAssignableFrom(clazz)) { setCellRenderer(i, new FromTableCellRenderer(fromWidgetfactory.create(RemoteWidgetType.TABLE))); setCellEditor(i, new FromTableCellRenderer(fromWidgetfactory.create(RemoteWidgetType.TABLE))); } } // Set specific column renderers for selected category. switch (selectedCategory) { case ALL: setCellRenderer(AllTableFormat.SIZE_INDEX, fileSizeRenderer.get()); break; case AUDIO: setHeaderRenderer(AudioTableFormat.LENGTH_INDEX, new TableCellHeaderRenderer(JLabel.TRAILING)); setCellRenderer(AudioTableFormat.SIZE_INDEX, fileSizeRenderer.get()); setCellRenderer(AudioTableFormat.LENGTH_INDEX, timeRenderer.get()); setCellRenderer(AudioTableFormat.QUALITY_INDEX, qualityRenderer.get()); break; case VIDEO: setHeaderRenderer(VideoTableFormat.LENGTH_INDEX, new TableCellHeaderRenderer(JLabel.TRAILING)); setCellRenderer(VideoTableFormat.SIZE_INDEX, fileSizeRenderer.get()); setCellRenderer(VideoTableFormat.LENGTH_INDEX, timeRenderer.get()); setCellRenderer(VideoTableFormat.QUALITY_INDEX, qualityRenderer.get()); break; case DOCUMENT: setCellRenderer(DocumentTableFormat.SIZE_INDEX, fileSizeRenderer.get()); break; case IMAGE: setCellRenderer(ImageTableFormat.SIZE_INDEX, fileSizeRenderer.get()); break; case PROGRAM: setCellRenderer(ProgramTableFormat.SIZE_INDEX, fileSizeRenderer.get()); break; case OTHER: setCellRenderer(OtherTableFormat.SIZE_INDEX, fileSizeRenderer.get()); break; case TORRENT: MultilineTooltipRenderer renderer = new MultilineTooltipRenderer(); setCellRenderer(TorrentTableFormat.SIZE_INDEX, fileSizeRenderer.get()); setCellRenderer(TorrentTableFormat.FILES_INDEX, renderer); setCellRenderer(TorrentTableFormat.TRACKERS_INDEX, renderer); setUnsortable(TorrentTableFormat.TRACKERS_INDEX); setUnsortable(TorrentTableFormat.FILES_INDEX); default: break; } } /** Prevents the given column from being sortable. */ private void setUnsortable(int column) { ((TableColumnExt)resultsTable.getColumnModel().getColumn(column)).setSortable(false); } /** * Assigns the specified cell renderer to the specified column in the * Table view column model. */ protected void setCellRenderer(int column, TableCellRenderer cellRenderer) { TableColumnModel tcm = resultsTable.getColumnModel(); TableColumn tc = tcm.getColumn(column); tc.setCellRenderer(cellRenderer); } /** * Assigns the specified cell editor to the specified column in the * Table view column model. */ protected void setCellEditor(int column, TableCellEditor editor) { TableColumnModel tcm = resultsTable.getColumnModel(); TableColumn tc = tcm.getColumn(column); tc.setCellEditor(editor); } /** * Assigns the specified header renderer to the specified column in the * Table view column model. */ protected void setHeaderRenderer(int column, TableCellRenderer headerRenderer) { TableColumnModel tcm = resultsTable.getColumnModel(); TableColumn tc = tcm.getColumn(column); tc.setHeaderRenderer(headerRenderer); } /** * Displays search results for the specified search category. */ public void showCategory(SearchCategory searchCategory) { if(currentCategory != searchCategory) { currentCategory = searchCategory; // Select category to update sorted list. searchResultsModel.setSelectedCategory(searchCategory); // Reconfigure the lists for the new category. // If one hasn't been configured yet, only configure // the visible one. (We reconfigure after it's already // been configured because it's easy. It would be // better to unconfigure the invisible one and // configure it only when it becomes visible.) if(listConfiguredFor != null || visibleComponent == resultsList) { configureList(); } if(tableConfiguredFor != null || visibleComponent == resultsTable) { configureTable(); } } else { LOG.debugf("Resetting current category {0}!", currentCategory); } } /** * Changes whether the list view or table view is displayed. * @param mode LIST or TABLE */ public void setViewType(SearchViewType mode) { if(visibleComponent != null) { remove((Component)visibleComponent); } switch (mode) { case LIST: // Only reconfigure when changing the view if it's configured // for the wrong category... if(currentCategory != null && listConfiguredFor != currentCategory) { configureList(); } this.visibleComponent = resultsList; break; case TABLE: if(currentCategory != null && tableConfiguredFor != currentCategory) { configureTable(); } this.visibleComponent = resultsTable; break; default: throw new IllegalStateException("unsupported mode: " + mode); } add((Component)visibleComponent); } /** * Returns the header component for the scroll pane. The method returns * null if no header is displayed. */ public Component getScrollPaneHeader() { return visibleComponent == resultsTable ? resultsTable.getTableHeader() : null; } /** * Returns the results view component currently being displayed. */ public Scrollable getScrollable() { return visibleComponent; } /**Disposes of the selection model when the result model is disposed*/ private class ResultModelDisposalListener implements DisposalListener { @Override public void objectDisposed(Disposable source) { if (listSelectionModel != null){ listSelectionModel.dispose(); } if(selectionModel != null){ selectionModel.dispose(); } } } /** * Paints the foreground of a table row. */ private static class DownloadedHighlightPredicate implements HighlightPredicate { private EventList<VisualSearchResult> eventList; public DownloadedHighlightPredicate (EventList<VisualSearchResult> sortedList) { this.eventList = sortedList; } @Override public boolean isHighlighted(Component renderer, ComponentAdapter adapter) { VisualSearchResult result = eventList.get(adapter.row); return result.isSpam(); } } /** * Table component to display search results in a vertical list. */ public static class ListViewTable extends ResultsTable<VisualSearchResult> { @Resource private Color similarResultParentBackgroundColor; private boolean ignoreRepaints; public ListViewTable() { super(); GuiUtils.assignResources(this); setGridColor(Color.decode("#EBEBEB")); setHighlighters( new ColorHighlighter(getBackground(), null, getTableColors().selectionColor, null), new ColorHighlighter(new HighlightPredicate() { public boolean isHighlighted(Component renderer, ComponentAdapter adapter) { VisualSearchResult vsr = (VisualSearchResult)getValueAt(adapter.row, 0); return vsr != null && vsr.isChildrenVisible(); }}, similarResultParentBackgroundColor, null, getTableColors().selectionColor, null)); } @Override protected void paintEmptyRows(Graphics g) { // do nothing. } private void setIgnoreRepaints(boolean ignore) { this.ignoreRepaints = ignore; } @Override protected void updateViewSizeSequence() { if (ignoreRepaints) { return; } super.updateViewSizeSequence(); } @Override protected void resizeAndRepaint() { if (ignoreRepaints) { return; } super.resizeAndRepaint(); } } }