package com.limegroup.gnutella.gui.search; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Date; import java.util.LinkedList; import java.util.List; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.OverlayLayout; import com.limegroup.gnutella.BrowseHostHandler; import com.limegroup.gnutella.FileDetails; import com.limegroup.gnutella.GUID; import com.limegroup.gnutella.MediaType; import com.limegroup.gnutella.RemoteFileDesc; import com.limegroup.gnutella.RouterService; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.gui.BoxPanel; import com.limegroup.gnutella.gui.FileDetailsProvider; import com.limegroup.gnutella.gui.GUIConstants; import com.limegroup.gnutella.gui.GUIMediator; import com.limegroup.gnutella.gui.IconButton; import com.limegroup.gnutella.gui.LicenseWindow; import com.limegroup.gnutella.gui.PaddedPanel; import com.limegroup.gnutella.gui.ProgTabUIFactory; import com.limegroup.gnutella.gui.tables.AbstractTableMediator; import com.limegroup.gnutella.gui.tables.ColumnPreferenceHandler; import com.limegroup.gnutella.gui.tables.DataLine; import com.limegroup.gnutella.gui.tables.LimeJTable; import com.limegroup.gnutella.gui.tables.LimeTableColumn; import com.limegroup.gnutella.gui.tables.TableSettings; import com.limegroup.gnutella.licenses.License; import com.limegroup.gnutella.licenses.VerificationListener; import com.limegroup.gnutella.search.QueryHandler; import com.limegroup.gnutella.settings.FilterSettings; import com.limegroup.gnutella.settings.SearchSettings; import com.limegroup.gnutella.xml.LimeXMLDocument; public class ResultPanel extends AbstractTableMediator implements VerificationListener, FileDetailsProvider { /** * Flag that a search has been stopped with a random GUID */ static final GUID STOPPED_GUID = new GUID(GUID.makeGuid()); private static final DateRenderer DATE_RENDERER = new DateRenderer(); private static final QualityRenderer QUALITY_RENDERER = new QualityRenderer(); private static final EndpointRenderer ENDPOINT_RENDERER = new EndpointRenderer(); private static final ResultSpeedRenderer RESULT_SPEED_RENDERER = new ResultSpeedRenderer(); private static final PercentageRenderer PERCENTAGE_RENDERER = new PercentageRenderer(); /** * The TableSettings that all ResultPanels will use. */ static final TableSettings SEARCH_SETTINGS = new TableSettings("SEARCH_TABLE"); /** * The search info of this class. */ private final SearchInformation SEARCH_INFO; /** * This' spam filter */ private final SpamFilter SPAM_FILTER; /** * The GUID of the last search. (Use this to match up results.) * May be a DummyGUID for the empty result list hack. */ private volatile GUID guid; /** * The time (in milliseconds) that we last received a Query Result */ private long timeLastResultReceived; /** * The BrowseHostHandler if this is a Browse Host tab. */ private BrowseHostHandler browseHandler = null; /** * Start time of the query that this specific ResultPane handles */ private long startTime = System.currentTimeMillis(); /** * The CompositeFilter for this ResultPanel. */ private CompositeFilter FILTER; /** * The download listener. */ ActionListener DOWNLOAD_LISTENER; /** * The "download as" listener. */ ActionListener DOWNLOAD_AS_LISTENER; /** * The chat listener. */ ActionListener CHAT_LISTENER; /** * The browse host listener. */ ActionListener BROWSE_HOST_LISTENER; /** * The stop listener. */ ActionListener STOP_LISTENER; /** * The Mark As Spam listener */ ActionListener MARK_AS_SPAM_LISTENER; /** * The Mark As Not Spam listener */ ActionListener MARK_AS_NOT_SPAM_LISTENER; /** * The button that marks search results as spam or undoes it */ private JButton SPAM_BUTTON; /** * Specialized constructor for creating a "dummy" result panel. * This should only be called once at search window creation-time. */ ResultPanel(JPanel overlay) { super("SEARCH_TABLE"); setupFakeTable(overlay); SEARCH_INFO = SearchInformation.createKeywordSearch("", null, MediaType.getAnyTypeMediaType()); SPAM_FILTER=null; FILTER = null; this.guid = STOPPED_GUID; setButtonEnabled(SearchButtons.STOP_BUTTON_INDEX, false); } /** * Constructs a new ResultPanel for search results. * * @param guid the guid of the query. Used to match results. * @param info the info of the search */ ResultPanel(GUID guid, SearchInformation info) { super("SEARCH_TABLE"); SEARCH_INFO = info; if (SEARCH_INFO.isBrowseHostSearch() || SEARCH_INFO.isWhatsNewSearch()) SPAM_FILTER = null; else SPAM_FILTER = new SpamFilter(); this.guid = guid; setupRealTable(); resetFilters(); } /** * Sets the default renderers to be used in the table. */ protected void setDefaultRenderers() { super.setDefaultRenderers(); TABLE.setDefaultRenderer(QualityHolder.class, QUALITY_RENDERER); TABLE.setDefaultRenderer(EndpointHolder.class, ENDPOINT_RENDERER); TABLE.setDefaultRenderer(ResultSpeed.class, RESULT_SPEED_RENDERER); TABLE.setDefaultRenderer(Date.class, DATE_RENDERER); TABLE.setDefaultRenderer(Float.class, PERCENTAGE_RENDERER); } /** * Does nothing. */ protected void updateSplashScreen() { } /** * Simple inner class to allow a PaddedPanel to implement Progressor. * This is necessary for the ProgTabUIFactory to get the percentage * of its tabs. */ private class PPP extends PaddedPanel implements ProgTabUIFactory.Progressor { public double calculatePercentage(long now) { return ResultPanel.this.calculatePercentage(now); } } /** * Sets up the constants: * FILTER, MAIN_PANEL, DATA_MODEL, TABLE, BUTTON_ROW. */ protected void setupConstants() { FILTER = new CompositeFilter(4); MAIN_PANEL = new PPP(); DATA_MODEL = new TableRowFilter(FILTER); TABLE = new LimeJTable(DATA_MODEL); ((ResultPanelModel)DATA_MODEL).setTable(TABLE); BUTTON_ROW = new SearchButtons(this).getComponent(); // The initialization of the SPAM_BUTTON is a bit // hackish. Use the NOT_SPAM label as it is longer // and needs thus more space. As next init the button // with the true label but keep the button width. See // transformButton() for more info... SPAM_BUTTON = new IconButton( GUIMediator.getStringResource("SEARCH_NOT_SPAM_BUTTON_LABEL"), "SEARCH_SPAM"); transformSpamButton(GUIMediator.getStringResource("SEARCH_SPAM_BUTTON_LABEL"), GUIMediator.getStringResource("SEARCH_SPAM_BUTTON_TIP")); SPAM_BUTTON.setEnabled(false); SPAM_BUTTON.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { TableLine[] lines = getAllSelectedLines(); if (lines.length > 0) { if (SpamFilter.isAboveSpamThreshold(lines[0])) { MARK_AS_NOT_SPAM_LISTENER.actionPerformed(e); } else { MARK_AS_SPAM_LISTENER.actionPerformed(e); } } } }); } /** * Sets SETTINGS to be the static SEARCH_SETTINGS, instead * of constructing a new one for each ResultPanel. */ protected void buildSettings() { SETTINGS = SEARCH_SETTINGS; } /** * Creates the specialized SearchColumnSelectionMenu menu, * which groups XML columns together. */ protected JPopupMenu createColumnSelectionMenu() { return (new SearchColumnSelectionMenu(TABLE)).getComponent(); } /** * Creates the specialized column preference handler for search columns. */ protected ColumnPreferenceHandler createDefaultColumnPreferencesHandler() { return new SearchColumnPreferenceHandler(TABLE); } /** * Sets DOWNLOAD_LISTENER, CHAT_LISTENER, BROWSE_HOST_LISTENER, * and STOP_LISTENER. */ protected void buildListeners() { super.buildListeners(); DOWNLOAD_LISTENER = new ActionListener() { public void actionPerformed(ActionEvent e) { SearchMediator.doDownload(ResultPanel.this); } }; DOWNLOAD_AS_LISTENER = new ActionListener() { public void actionPerformed(ActionEvent e) { SearchMediator.doDownloadAs(ResultPanel.this); } }; CHAT_LISTENER = new ActionListener() { public void actionPerformed(ActionEvent e) { doChat(); } }; BROWSE_HOST_LISTENER = new ActionListener() { public void actionPerformed(ActionEvent e) { SearchMediator.doBrowseHost(ResultPanel.this); } }; STOP_LISTENER = new ActionListener() { public void actionPerformed(ActionEvent e) { stopSearch(); } }; MARK_AS_SPAM_LISTENER = new ActionListener() { public void actionPerformed(ActionEvent e) { TableLine[] lines = getAllSelectedLines(); for (int i = 0; i < lines.length; i++) { SPAM_FILTER.markAsSpamUser(lines[i], true); } // This is a bit fine tuning... if (SearchSettings.hideJunk()) { filtersChanged(); // i.e. hide the search result(s) we've just // marked as spam } else { DATA_MODEL.refresh(); // mark 'em red transformSpamButton(GUIMediator.getStringResource("SEARCH_NOT_SPAM_BUTTON_LABEL"), GUIMediator.getStringResource("SEARCH_NOT_SPAM_BUTTON_TIP")); } } }; MARK_AS_NOT_SPAM_LISTENER = new ActionListener() { public void actionPerformed(ActionEvent e) { TableLine[] lines = getAllSelectedLines(); for (int i = 0; i < lines.length; i++) { SPAM_FILTER.markAsSpamUser(lines[i], false); } DATA_MODEL.refresh(); transformSpamButton(GUIMediator.getStringResource("SEARCH_SPAM_BUTTON_LABEL"), GUIMediator.getStringResource("SEARCH_SPAM_BUTTON_TIP")); } }; } /** * Creates the specialized SearchResultMenu for right-click popups. * * Upgraded access from protected to public for SearchResultDisplayer. */ public JPopupMenu createPopupMenu() { // do not return a menu if right-clicking on the dummy panel if (!isKillable()) return null; TableLine[] lines = getAllSelectedLines(); boolean allSpam = true; boolean allNot = true; if (SPAM_FILTER != null) { for (int i = 0; i < lines.length; i++) { if (!SpamFilter.isAboveSpamThreshold(lines[i])) allSpam = false; else allNot = false; } } return (new SearchResultMenu(this)).createMenu(lines, !allSpam, !allNot); } /** * Adds a single result. * * Also marks the last time a result was received. */ public void add(Object o) { super.add(o); timeLastResultReceived = System.currentTimeMillis(); } /** * Do not allow removal of rows. */ public void removeSelection() { } /** * Clears the table and converts the download button into a * wishlist button. */ public void clearTable() { super.clearTable(); } /** * Sets the appropriate buttons to be disabled. */ public void handleNoSelection() { setButtonEnabled(SearchButtons.DOWNLOAD_BUTTON_INDEX, false); setButtonEnabled(SearchButtons.BROWSE_BUTTON_INDEX, false); SPAM_BUTTON.setEnabled(false); if (SearchSettings.ENABLE_SPAM_FILTER.getValue() && SPAM_FILTER != null) { transformSpamButton(GUIMediator.getStringResource("SEARCH_SPAM_BUTTON_LABEL"), GUIMediator.getStringResource("SEARCH_SPAM_BUTTON_TIP")); } } /** * Sets the appropriate buttons to be enabled. */ public void handleSelection(int i) { setButtonEnabled(SearchButtons.DOWNLOAD_BUTTON_INDEX, true); TableLine line = (TableLine)DATA_MODEL.get(i); setButtonEnabled(SearchButtons.BROWSE_BUTTON_INDEX, line.isBrowseHostEnabled()); if (SearchSettings.ENABLE_SPAM_FILTER.getValue() && SPAM_FILTER != null) { SPAM_BUTTON.setEnabled(true); if (SpamFilter.isAboveSpamThreshold(line)) { transformSpamButton(GUIMediator.getStringResource("SEARCH_NOT_SPAM_BUTTON_LABEL"), GUIMediator.getStringResource("SEARCH_NOT_SPAM_BUTTON_TIP")); } else { transformSpamButton(GUIMediator.getStringResource("SEARCH_SPAM_BUTTON_LABEL"), GUIMediator.getStringResource("SEARCH_SPAM_BUTTON_TIP")); } } } /** * Forwards the event to DOWNLOAD_LISTENER. */ public void handleActionKey() { DOWNLOAD_LISTENER.actionPerformed(null); } /** * Gets the SearchInformation of this search. */ SearchInformation getSearchInformation() { return SEARCH_INFO; } /** * Gets the query of the search. */ String getQuery() { return SEARCH_INFO.getQuery(); } /** * Returns the title of the search. * @return */ String getTitle() { return SEARCH_INFO.getTitle(); } /** * Gets the rich query of the search. */ String getRichQuery() { return SEARCH_INFO.getXML(); } /** * Stops this result panel from receiving more results. */ void stopSearch() { final GUID guidToStop = guid; GUIMediator.instance().schedule(new Runnable() { public void run() { RouterService.stopQuery(guidToStop); } }); setGUID(STOPPED_GUID); SearchMediator.checkToStopLime(); setButtonEnabled(SearchButtons.STOP_BUTTON_INDEX, false); } /** * Chats with the host chat-enabled host in the selected * TableLine. */ void doChat() { TableLine line = getSelectedLine(); if(line == null) return; if(!line.isChatEnabled()) return; line.doChat(); } /** * Blocks the host that sent the selected result. */ void blockHost() { TableLine line = getSelectedLine(); if(line == null) return; String host = line.getHostname(); int answer = GUIMediator.showYesNoMessage("SEARCH_BLOCK_HOST", " " + host + "?"); if (answer == GUIMediator.YES_OPTION && host != null) { String[] bannedIps = FilterSettings.BLACK_LISTED_IP_ADDRESSES.getValue(); // Ignore if this host is already banned. for (int i = 0; i < bannedIps.length; i++) if (host.equalsIgnoreCase(bannedIps[i])) return; String[] newBannedIps = new String[bannedIps.length + 1]; System.arraycopy(bannedIps, 0, newBannedIps, 0, bannedIps.length); newBannedIps[bannedIps.length] = host; FilterSettings.BLACK_LISTED_IP_ADDRESSES.setValue(newBannedIps); RouterService.adjustSpamFilters(); } } /** * Shows a LicenseWindow for the selected line. */ void showLicense() { TableLine line = getSelectedLine(); if(line == null) return; URN urn = line.getSHA1Urn(); LimeXMLDocument doc = line.getXMLDocument(); LicenseWindow window = LicenseWindow.create(line.getLicense(), urn, doc, this); window.setVisible(true); } public void licenseVerified(License license) { // if it was valid at all, refresh. if(license.isValid(null)) ((ResultPanelModel)DATA_MODEL).slowRefresh(); } /** * Determines whether or not this panel is stopped. */ boolean isStopped() { return guid.equals(STOPPED_GUID); } /** * Determines if this is empty. */ boolean isEmpty() { return DATA_MODEL.getRowCount() == 0; } /** * Determines if this can be removed. */ boolean isKillable() { // the dummy panel has a null filter, and is the only one not killable return FILTER != null; } /** * Notification that a filter on this panel has changed. * * Updates the data model with the new list, maintains the selection, * and moves the viewport to the first still visible selected row. * * Note that the viewport moving cannot be done by just storing the first * visible row, because after the filters change, the row might not exist * anymore. Thus, it is necessary to store all visible rows and move to * the first still-visible one. */ boolean filterChanged(TableLineFilter filter, int depth) { if(!FILTER.setFilter(depth, filter)) return false; // store the selection & visible rows int[] rows = TABLE.getSelectedRows(); DataLine[] lines = new DataLine[rows.length]; List inView = new LinkedList(); for(int i = 0; i < rows.length; i++) { int row = rows[i]; DataLine line = DATA_MODEL.get(row); lines[i] = line; if(TABLE.isRowVisible(row)) inView.add(line); } // change the table. ((TableRowFilter)DATA_MODEL).filtersChanged(); // reselect & move the viewpoint to the first still visible row. for(int i = 0; i < rows.length; i++) { DataLine line = lines[i]; int row = DATA_MODEL.getRow(line); if(row != -1) { TABLE.addRowSelectionInterval(row, row); if(inView != null && inView.contains(line)) { TABLE.ensureRowVisible(row); inView = null; } } } // update the tab count. SearchMediator.setTabDisplayCount(this); return true; } /** * Returns the total number of sources found for this search. */ int totalSources() { return ((ResultPanelModel)DATA_MODEL).getTotalSources(); } /** * Returns the total number of filtered source found for this search. */ int filteredSources() { return ((TableRowFilter)DATA_MODEL).getFilteredSources(); } /** * Determines whether or not repeat search is currently enabled. * Repeat search will be disabled if, for example, the original * search was performed too recently. * * @return <tt>true</tt> if the repeat search feature is currently * enabled, otherwise <tt>false</tt> */ boolean isRepeatSearchEnabled() { return FILTER != null; } void repeatSearch() { clearTable(); startTime = System.currentTimeMillis(); resetFilters(); SearchMediator.setTabDisplayCount(this); SearchMediator.repeatSearch(this, SEARCH_INFO); setButtonEnabled(SearchButtons.STOP_BUTTON_INDEX, true); } void resetFilters() { FILTER.reset(); if (!SEARCH_INFO.isBrowseHostSearch() && !SEARCH_INFO.isWhatsNewSearch()) { ((TableRowFilter)DATA_MODEL).setJunkFilter(SPAM_FILTER); } else { ((TableRowFilter)DATA_MODEL).setJunkFilter(null); } } private void filtersChanged() { ((TableRowFilter)DATA_MODEL).filtersChanged(); SearchMediator.setTabDisplayCount(this); } /** * Gets the MetadataModel used for results. */ MetadataModel getMetadataModel() { return ((ResultPanelModel)DATA_MODEL).getMetadataModel(); } /** Returns true if this is responsible for results with the given GUID */ boolean matches(GUID otherGuid) { return this.guid.equals(otherGuid); } /** * @modifies this * @effects sets this' guid. This is needed for browse host functionality. */ void setGUID(GUID guid) { this.guid=guid; } /** Returns the guid this is responsible for. */ byte[] getGUID() { return guid.bytes(); } /** Returns the media type this is responsible for. */ MediaType getMediaType() { return SEARCH_INFO.getMediaType(); } /** * Sets the BrowseHostHandler. */ void setBrowseHostHandler(BrowseHostHandler bhh) { browseHandler = bhh; } /** * Gets all currently selected TableLines. * * @return empty array if no lines are selected. */ TableLine[] getAllSelectedLines() { int[] rows = TABLE.getSelectedRows(); if(rows == null) return new TableLine[0]; TableLine[] lines = new TableLine[rows.length]; for(int i = 0; i < rows.length; i++) lines[i] = (TableLine)DATA_MODEL.get(rows[i]); return lines; } /** * Gets the currently selected TableLine. * * @return null if there is no selected line. */ TableLine getSelectedLine() { int selected = TABLE.getSelectedRow(); if(selected != -1) return (TableLine)DATA_MODEL.get(selected); else return null; } /** * Calculates the percentange of results that have been received for this * ResultPanel. */ double calculatePercentage(long currentTime) { if(guid.equals(STOPPED_GUID)) return 1d; if(SEARCH_INFO.isBrowseHostSearch()) { if( browseHandler != null ) return browseHandler.getPercentComplete(currentTime); else return 0d; } // first calculate the percentage solely based on // the number of results we've received. int ideal = QueryHandler.ULTRAPEER_RESULTS; double resultPerc = (double)totalSources() / ideal; // then calculate the percentage solely based on // the time we've spent querying. long spent = currentTime - startTime; double timePerc = (double)spent / QueryHandler.MAX_QUERY_TIME; // If the results are already enough to fill it up, just use that. if( resultPerc >= 1 ) return 1d; // Otherwise, the time percentage should fill up what remains in // the progress. timePerc = timePerc * (1 - resultPerc); // Return the results received + time spent. return resultPerc + timePerc; } /** * Sets extra values for non dummy ResultPanels. * (Used for all tables that will have results.) * * Currently: * - Sorts the count column, if it is visible & real-time sorting is on. * - Adds listeners, so the filters can be displayed when necessary. */ private void setupRealTable() { SearchTableColumns columns = ((ResultPanelModel)DATA_MODEL).getColumns(); LimeTableColumn countColumn = columns.getColumn(SearchTableColumns.COUNT_IDX); if(SETTINGS.REAL_TIME_SORT.getValue() && TABLE.isColumnVisible(countColumn.getId())) { DATA_MODEL.sort(SearchTableColumns.COUNT_IDX); // ascending DATA_MODEL.sort(SearchTableColumns.COUNT_IDX); // descending } MouseListener filterDisplayer = new MouseListener() { public void mouseClicked(MouseEvent e) { if(e.isConsumed()) return; e.consume(); SearchMediator.panelSelected(ResultPanel.this); } public void mousePressed(MouseEvent e) {} public void mouseReleased(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} }; // catches around the button area. MAIN_PANEL.addMouseListener(filterDisplayer); // catches the blank area before results fill in SCROLL_PANE.addMouseListener(filterDisplayer); // catches selections on the table TABLE.addMouseListener(filterDisplayer); // catches the table header TABLE.getTableHeader().addMouseListener(filterDisplayer); } /** * Overwritten */ protected void setupMainPanel() { if (SearchSettings.ENABLE_SPAM_FILTER.getValue() && MAIN_PANEL != null) { MAIN_PANEL.add(getScrolledTablePane()); addButtonRow(); MAIN_PANEL.setMinimumSize(ZERO_DIMENSION); } else { super.setupMainPanel(); } } /** * Adds the overlay panel into the table & converts the button * to 'download'. */ private void setupFakeTable(JPanel overlay) { MAIN_PANEL.removeAll(); JPanel background = new JPanel(); background.setLayout(new OverlayLayout(background)); JPanel overlayPanel = new BoxPanel(BoxPanel.Y_AXIS); overlayPanel.setOpaque(false); overlayPanel.add(Box.createVerticalStrut(20)); overlayPanel.add(overlay); overlayPanel.setMinimumSize(new Dimension(0, 0)); JComponent table = getScrolledTablePane(); table.setOpaque(false); background.add(overlayPanel); background.add(table); MAIN_PANEL.add(background); addButtonRow(); MAIN_PANEL.setMinimumSize(ZERO_DIMENSION); } /** * Adds the button row and the Spam Button */ private void addButtonRow() { if (BUTTON_ROW != null) { MAIN_PANEL.add(Box.createVerticalStrut(GUIConstants.SEPARATOR)); if (SearchSettings.ENABLE_SPAM_FILTER.getValue() && SPAM_BUTTON != null) { JPanel buttonPanel = new JPanel(); buttonPanel.setOpaque(false); buttonPanel.setLayout(new GridBagLayout()); GridBagConstraints gbc = null; gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.CENTER; gbc.fill = GridBagConstraints.NONE; gbc.gridwidth = GridBagConstraints.RELATIVE; gbc.weightx = 1; buttonPanel.add(BUTTON_ROW, gbc); gbc = new GridBagConstraints(); gbc.gridx = 1; gbc.gridy = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.NONE; gbc.gridwidth = GridBagConstraints.REMAINDER; buttonPanel.add(SPAM_BUTTON, gbc); buttonPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 64)); MAIN_PANEL.add(buttonPanel); } else { MAIN_PANEL.add(BUTTON_ROW); } } } public FileDetails[] getFileDetails() { int[] sel = TABLE.getSelectedRows(); ArrayList list = new ArrayList(sel.length); for (int i = 0; i < sel.length; i++) { TableLine line = (TableLine)DATA_MODEL.get(sel[i]); // prefer non-firewalled rfds for the magnet action RemoteFileDesc rfd = line.getNonFirewalledRFD(); if (rfd != null) { list.add(rfd); } else { // fall back on first rfd rfd = line.getRemoteFileDesc(); if (rfd != null) { list.add(rfd); } } } if (list.isEmpty()) { return new FileDetails[0]; } return (FileDetails[])list.toArray(new FileDetails[0]); } /** * Change the text and tooltip text of the SPAM_BUTTON */ private void transformSpamButton(String text, String tip) { Dimension oldDim = SPAM_BUTTON.getPreferredSize(); SPAM_BUTTON.setText(text); SPAM_BUTTON.setToolTipText(tip); // Preserve/use the max width... Dimension newDim = SPAM_BUTTON.getPreferredSize(); newDim.width = Math.max(oldDim.width, newDim.width); SPAM_BUTTON.setPreferredSize(newDim); } }