package com.limegroup.gnutella.gui.search; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.IllegalComponentStateException; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.plaf.TabbedPaneUI; import com.limegroup.gnutella.GUID; import com.limegroup.gnutella.RouterService; import com.limegroup.gnutella.gui.BoxPanel; import com.limegroup.gnutella.gui.GUIMediator; import com.limegroup.gnutella.gui.ProgTabUIFactory; import com.limegroup.gnutella.gui.RefreshListener; import com.limegroup.gnutella.settings.QuestionsHandler; import com.limegroup.gnutella.settings.SearchSettings; import com.limegroup.gnutella.gui.themes.ThemeMediator; import com.limegroup.gnutella.gui.themes.ThemeObserver; /** * This class handles the dislay of search results. */ final class SearchResultDisplayer implements ThemeObserver, RefreshListener { /** * <tt>JPanel</tt> containting the primary components of the search result * display. */ private final JPanel MAIN_PANEL = new BoxPanel(BoxPanel.Y_AXIS); /** * The main tabbed pane for displaying different search results. */ private final JTabbedPane TABBED_PANE = new JTabbedPane(); /** The contents of tabbedPane. * INVARIANT: entries.size()==# of tabs in tabbedPane * LOCKING: +obtain entries' monitor before adjusting number of * outstanding searches, i.e., the number of tabs * +obtain a ResultPanel's monitor before adding or removing * results + to prevent deadlock, never obtain ResultPanel's * lock if holding entries'. */ private static final List /* of ResultPanel */ entries = new ArrayList(); /** Results is a panel that displays either a JTabbedPane when lots of * results exist OR a blank ResultPanel when nothing is showing. * Use switcher to switch between the two. The first entry is the * blank results panel; the second is the tabbed panel. */ private final JPanel results = new JPanel(); /** * The layout that switches between the dummy result panel * and the JTabbedPane. */ private final CardLayout switcher = new CardLayout(); /** * The dummy result panel, used when no searches are active. */ private final ResultPanel DUMMY; /** * The overlay panel to use when no searches are active. */ private final OverlayAd OVERLAY; /** * The listener to notify about the currently displaying search * changing. * * TODO: Allow more than one. */ private ChangeListener _activeSearchListener; /** * Listener for events on the tabbedpane. */ private final PaneListener PANE_LISTENER = new PaneListener(); /** * Constructs the search display elements. */ SearchResultDisplayer() { MAIN_PANEL.setMinimumSize(new Dimension(0,0)); ProgTabUIFactory.extendUI(TABBED_PANE); TABBED_PANE.setRequestFocusEnabled(false); // make the results panel take up as much space as possible // for when the window is resized. results.setPreferredSize(new Dimension(10000, 10000)); results.setLayout(switcher); OVERLAY = new OverlayAd(); DUMMY = new ResultPanel(OVERLAY); JPanel mainScreen = new JPanel(new BorderLayout()); mainScreen.add(DUMMY.getComponent(), BorderLayout.CENTER); results.add("dummy", mainScreen); results.add("tabbedPane",TABBED_PANE); switcher.first(results); MAIN_PANEL.add(results); TABBED_PANE.addMouseListener(PANE_LISTENER); TABBED_PANE.addMouseMotionListener(PANE_LISTENER); TABBED_PANE.addChangeListener(PANE_LISTENER); ThemeMediator.addThemeObserver(this); CancelSearchIconProxy.updateTheme(); } /** * Sets the listener for what searches are currently displaying. */ void setSearchListener(ChangeListener listener) { _activeSearchListener = listener; } /** * Iterates through each displayed ResultPanel and fires an update. */ void updateResults() { for(int i = 0; i < entries.size(); i++) ((ResultPanel)entries.get(i)).refresh(); } /** * @modifies tabbed pane, entries * @effects adds an entry for a search for stext with GUID guid * to the tabbed pane. This is used both for normal searching * and browsing. Returns the ResultPanel added. */ ResultPanel addResultTab(GUID guid, SearchInformation info) { final ResultPanel panel=new ResultPanel(guid, info); entries.add(panel); TABBED_PANE.addTab(info.getTitle(), CancelSearchIconProxy.createSelected(), panel.getComponent()); TABBED_PANE.setSelectedIndex(entries.size()-1); //Remove an old search if necessary if (entries.size() > SearchSettings.PARALLEL_SEARCH.getValue()) killSearchAtIndex(0); GUIMediator.instance().setSearching(true); OVERLAY.searchPerformed(); switcher.last(results); //show tabbed results // If there are lots of tabs, this ensures everything // is properly visible. MAIN_PANEL.revalidate(); return panel; } /** * If i rp is no longer the i'th panel of this, returns silently. Otherwise * adds line to rp under the given group. Updates the count on the tab in * this and restarts the spinning lime. * @requires this is called from Swing thread, group is null or similar * to line and already in rp * @modifies this */ void addQueryResult(byte[] replyGUID, SearchResult line, ResultPanel rp) { //Actually add the line. Must obtain rp's monitor first. if(!rp.matches(new GUID(replyGUID)))//GUID of rp!=replyGuid throw new IllegalArgumentException("guids don't match"); rp.add(line); int resultPanelIndex = -1; // Search for the ResultPanel to verify it exists. resultPanelIndex = entries.indexOf(rp); // If we couldn't find it, silently exit. if( resultPanelIndex == -1 ) return; //Update index on tab. Don't forget to add 1 since line hasn't //actually been added! TABBED_PANE.setTitleAt(resultPanelIndex, titleOf(rp)); } /** * Adds the specified ChangeListener to the list of listeners * on the JTabbedPane. */ void addChangeListener(ChangeListener listener) { TABBED_PANE.addChangeListener(listener); } /** * Adds the specified FocusListener to the list of listeners * of the JTabbedPane. */ void addFocusListener(FocusListener listener) { TABBED_PANE.addFocusListener(listener); } /** * Shows the popup menu that displays various options to the user. */ void showMenu(MouseEvent e) { ResultPanel rp = getSelectedResultPanel(); if(rp != null) { JPopupMenu menu = rp.createPopupMenu(); Point p = e.getPoint(); if(menu != null) { try { menu.show(MAIN_PANEL, p.x+1, p.y-6); } catch(IllegalComponentStateException icse) { // happens occasionally, ignore. } } } } /** * Returns the currently selected <tt>ResultPanel</tt> instance. * * @return the currently selected <tt>ResultPanel</tt> instance, * or <tt>null</tt> if there is no currently selected panel */ ResultPanel getSelectedResultPanel() { int i=TABBED_PANE.getSelectedIndex(); if(i==-1) return null; try { return (ResultPanel)entries.get(i); } catch(IndexOutOfBoundsException e){ return null; } } /** * Returns the <tt>ResultPanel</tt> for the specified GUID. * * @param rguid the guid to search for * @return the ResultPanel that matches the specified GUID, or null * if none match. */ ResultPanel getResultPanelForGUID(GUID rguid) { for (int i=0; i<entries.size(); i++) { ResultPanel rp = (ResultPanel)entries.get(i); if (rp.matches(rguid)) //order matters: rp may be a dummy guid. return rp; } return null; } /** * Returns the <tt>ResultPanel</tt> at the specified index. * * @param index the index of the desired <tt>ResultPanel</tt> * @return the <tt>ResultPanel</tt> at the specified index */ ResultPanel getPanelAtIndex(int index) { return (ResultPanel)entries.get(index); } /** * Returns the index in the list of search panels that corresponds * to the specified guid, or -1 if the specified guid does not * exist. * * @param rguid the guid to search for * @return the index of the specified guid, or -1 if it does not * exist. */ int getIndexForGUID(GUID rguid) { for (int i=0; i<entries.size(); i++) { ResultPanel rp = (ResultPanel)entries.get(i); if (rp.matches(rguid)) //order matters: rp may be a dummy guid. return i; } return -1; } /** * Get index for point. */ int getIndexForPoint(int x, int y) { TabbedPaneUI ui = TABBED_PANE.getUI(); return ui.tabForCoordinate(TABBED_PANE, x, y); } /** * @modifies tabbed pane, entries * @effects removes the currently selected result window (if any) * from this */ void killSearch() { int i=TABBED_PANE.getSelectedIndex(); if (i==-1) //nothing selected?! return; killSearchAtIndex(i); } /** * @modifies tabbed pane, entries * @effects removes the window at i from this */ void killSearchAtIndex(int i) { ResultPanel killed = (ResultPanel) entries.remove(i); final GUID killedGUID = new GUID(killed.getGUID()); GUIMediator.instance().schedule(new Runnable() { public void run() { RouterService.stopQuery(killedGUID); } }); try { TABBED_PANE.removeTabAt(i); } catch(IllegalArgumentException iae) { // happens occasionally on osx w/ java 1.4.2_05, ignore. } fixIcons(); SearchMediator.searchKilled(killed); ThemeMediator.removeThemeObserver(killed); if (entries.size()==0) { try { switcher.first(results); //show dummy table } catch(ArrayIndexOutOfBoundsException aioobe) { //happens on jdk1.5 beta w/ windows XP, ignore. } GUIMediator.instance().setSearching(false); } else { checkToStopLime(); } } /** * Notification that a browse host for the given GUID has failed. * * Removes the panel associated with that search. */ void browseHostFailed(GUID guid) { int i = getIndexForGUID(guid); if (i > -1) { ResultPanel rp = getPanelAtIndex(i); killSearchAtIndex(i); GUIMediator.showError("ERROR_BROWSE_HOST_FAILED_BEGIN_KEY", rp.getTitle(), "ERROR_BROWSE_HOST_FAILED_END_KEY", QuestionsHandler.BROWSE_HOST_FAILED); } } /** * @modifies spinning lime state * @effects If all searches are stopped, then the Lime stops spinning. */ void checkToStopLime() { ResultPanel panel; long now = System.currentTimeMillis(); // Decide if we definitely can stop the lime boolean stopLime = true; for (int i=0; i<entries.size(); i++) { panel = (ResultPanel)entries.get(i); stopLime &= panel.isStopped() || panel.calculatePercentage(now) >= 1d; } if ( stopLime ) { GUIMediator.instance().setSearching(false); } } /** * called by ResultPanel when the views are changed. Used to set the * tab to indicate the correct number of TableLines in the current * view. */ void setTabDisplayCount(ResultPanel rp){ Object panel; int i=0; boolean found = false; for(;i<entries.size();i++){//safe its synchronized panel = entries.get(i); if (panel == rp){ found = true; break; } } if(found)//find the number of lines in model TABBED_PANE.setTitleAt(i, titleOf(rp)); } private void fixIcons() { int sel = TABBED_PANE.getSelectedIndex(); for(int i = 0; i < entries.size(); i++) { TABBED_PANE.setIconAt(i, i == sel ? CancelSearchIconProxy.createSelected() : CancelSearchIconProxy.createPlain()); } } /** * Accessor for the <tt>ResultPanel</tt> instance that shows no active * searches. * * @return the <tt>ResultPanel</tt> instance that shows no active * searches */ ResultPanel getDummyResultPanel(){ return DUMMY; } /** * Returns the <tt>JComponent</tt> instance containing all of the search * result ui components. * * @return the <tt>JComponent</tt> instance containing all of the search * result ui components */ JComponent getComponent() { return MAIN_PANEL; } // inherit doc comment public void updateTheme() { ProgTabUIFactory.extendUI(TABBED_PANE); DUMMY.updateTheme(); OVERLAY.updateTheme(); CancelSearchIconProxy.updateTheme(); fixIcons(); for(Iterator i = entries.iterator(); i.hasNext(); ) { ResultPanel curPanel = (ResultPanel)i.next(); curPanel.updateTheme(); } } /** * Every second, redraw only the tab portion of the TabbedPane * and determine if we should stop the lime spinning. */ public void refresh() { checkToStopLime(); if(TABBED_PANE.isVisible() && TABBED_PANE.isShowing()) { Rectangle allBounds = TABBED_PANE.getBounds(); Component comp = null; try { comp = TABBED_PANE.getSelectedComponent(); } catch(ArrayIndexOutOfBoundsException aioobe) { // happens on OSX occasionally, ignore. } if(comp != null) { Rectangle compBounds = comp.getBounds(); // The length of the tab rectangle will extend // over the bounds of the entire TabbedPane // up to 1 before the y scale of the visible component. Rectangle allTabs = new Rectangle(allBounds.x, allBounds.y, allBounds.width, compBounds.y-1); TABBED_PANE.repaint(allTabs); } } } /** * Returns the title of the specified ResultPanel. */ private String titleOf(ResultPanel rp) { int current = rp.filteredSources(); int total = rp.totalSources(); if(current < total) return rp.getTitle() + " (" + current + "/" + total + ")"; else return rp.getTitle() + " (" + total + ")"; } /** * Listens for events on the JTabbedPane and dispatches commands. */ private class PaneListener implements MouseListener, MouseMotionListener, ChangeListener { /** * The last index that was rolled over. */ private int lastIdx = -1; /** * Either closes the selected tab or notifies the listener * that a tab was clicked. */ public void mouseClicked(MouseEvent e) { if(tryPopup(e)) return; if(SwingUtilities.isLeftMouseButton(e)) { int x = e.getX(); int y = e.getY(); int idx; idx = shouldKillIndex(x, y); if(idx != -1) { lastIdx = -1; killSearchAtIndex(idx); } if(idx == -1) stateChanged(null); } } /** * Redoes the icons on the tab which this is over. */ public void mouseMoved(MouseEvent e) { int x = e.getX(); int y = e.getY(); int idx = shouldKillIndex(x, y); if(idx != lastIdx && lastIdx != -1) resetIcon(); if(idx != -1) { TABBED_PANE.setIconAt(idx, CancelSearchIconProxy.createArmed()); lastIdx = idx; } } /** * Returns the index of the tab if the coordinates x,y can close it. * Otherwises returns -1. */ private int shouldKillIndex(int x, int y) { int idx = getIndexForPoint(x, y); if(idx != -1) { Icon icon = TABBED_PANE.getIconAt(idx); if(icon != null && icon instanceof CancelSearchIconProxy) if(((CancelSearchIconProxy)icon).shouldKill(x, y)) return idx; } return -1; } /** * Resets the last armed icon. */ private void resetIcon() { if(lastIdx != -1 && lastIdx < TABBED_PANE.getTabCount()) { if(lastIdx == TABBED_PANE.getSelectedIndex()) TABBED_PANE.setIconAt(lastIdx, CancelSearchIconProxy.createSelected()); else TABBED_PANE.setIconAt(lastIdx, CancelSearchIconProxy.createPlain()); lastIdx = -1; } } public void mousePressed(MouseEvent e) { tryPopup(e); } public void mouseReleased(MouseEvent e) { tryPopup(e); } public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) { resetIcon(); } public void mouseDragged(MouseEvent e) {} /** * Shows thep popup if this was a popup trigger. */ private boolean tryPopup(final MouseEvent e) { if ( e.isPopupTrigger() ) { // make sure the given tab is selected. int idx = getIndexForPoint(e.getX(), e.getY()); if(idx != -1) TABBED_PANE.setSelectedIndex(idx); showMenu(e); return true; } return false; } /** * Forwards events to the activeSearchListener. */ public void stateChanged(ChangeEvent e) { _activeSearchListener.stateChanged(e); fixIcons(); } } }