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();
}
}
}