package com.limegroup.gnutella.gui.search;
import java.io.IOException;
import javax.swing.BoxLayout;
import javax.swing.JList;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.xml.sax.SAXException;
import com.limegroup.gnutella.MediaType;
import com.limegroup.gnutella.gui.BoxPanel;
import com.limegroup.gnutella.xml.LimeXMLDocument;
import com.limegroup.gnutella.xml.SchemaNotFoundException;
/**
* The Panel that contains the various FilterBoxes.
*/
class FilterPanel extends BoxPanel {
/**
* The current uniqueID.
*/
private static int uniqueIdCount = 0;
/**
* The unique identifier (different than the GUID, which can change)
* for this FilterPanel.
*/
private final String ID;
/**
* The first filter box.
*/
private final FilterBox BOX_1;
/**
* The second filter box.
*/
private final FilterBox BOX_2;
/**
* The third filter box.
*/
private final FilterBox BOX_3;
/**
* The ResultPanel this is filtering.
*/
private final ResultPanel RESULTS;
/**
* The MetadataModel this is getting selectors from.
*/
private final MetadataModel MODEL;
/**
* Constructs a new FilterPanel for the specified ResultPanel.
*
* The exact FilterBoxes added to FilterPanel are dependent on prior
* user preferences.
*
*/
public FilterPanel(final ResultPanel results) {
super(BoxLayout.Y_AXIS);
String searchType = results.getMediaType().getMimeType();
Selector one = SelectorsHandler.getSelector(searchType, 0);
Selector two = SelectorsHandler.getSelector(searchType, 1);
Selector three = SelectorsHandler.getSelector(searchType, 2);
MetadataModel model = results.getMetadataModel();
BOX_1 = new FilterBox(model, one);
BOX_2 = new FilterBox(model, two);
BOX_3 = new FilterBox(model, three);
RESULTS = results;
MODEL = model;
// Create a single listener for all boxes and dispatch
// the event depending on which box it came from.
new MinimizeManager(new SelectorListener());
add(BOX_1.getComponent());
add(BOX_2.getComponent());
add(BOX_3.getComponent());
setMatchingValues(null);
ID = "" + uniqueIdCount++;
}
/**
* Returns a description unique to this FilterPanel.
*/
String getUniqueDescription() {
return ID;
}
/**
* Returns an array of the three {@FilterBox filter boxes} managed by
* this FilterPanel.
* @return
*/
public FilterBox[] getBoxes() {
return new FilterBox[] { BOX_1, BOX_2, BOX_3 };
}
/**
* Attempts to set any matching values
* in the filter boxes. If 'box' is null, matches against
* all boxes. Otherwise matches only against 'box'.
*/
private void setMatchingValues(FilterBox box) {
SearchInformation info = RESULTS.getSearchInformation();
// only select on keyword searches.
if(info.isKeywordSearch()) {
LimeXMLDocument doc = null;
String xml = RESULTS.getRichQuery();
if(xml != null) {
try {
doc = new LimeXMLDocument(xml);
}
catch(SAXException se) {}
catch(SchemaNotFoundException snfe) {}
catch(IOException ioe) {}
}
if(box != null) {
selectMatchingValues(box, doc, info);
} else {
selectMatchingValues(BOX_1, doc, info);
selectMatchingValues(BOX_2, doc, info);
selectMatchingValues(BOX_3, doc, info);
}
}
}
/**
* Selects any values in the FilterBox if they match the document.
*/
private void selectMatchingValues(FilterBox box, LimeXMLDocument doc,
SearchInformation info) {
// only works for field selectors.
if(!box.getSelector().isFieldSelector())
return;
String xml = info.getXML();
String query = info.getQuery();
MediaType media = info.getMediaType();
String value = null;
// if there was a XMLDocument for searching, use that for matching
if(doc != null) {
// only select if the selector's schema matches the doc's schema
String schema = box.getSelector().getSchema();
if(!schema.equals(doc.getSchemaDescription()))
return;
String field = box.getSelector().getValue();
// Get the value of the selector's field from the document.
value = doc.getValue(field);
} else if(media == MediaType.getAnyTypeMediaType()) {
// otherwise, if they did an any-type search,
// try and match up in any of the boxes.
value = query;
}
// If we have a matching value, use it.
if(value != null) {
box.setRequestedValue(value);
}
}
/**
* Manages when minimizing the boxes is/isn't allowed.
*/
private class MinimizeManager implements ChangeListener {
final ChangeListener SAVER;
/**
* Adds itself as a stateChangeListener on the boxes.
*/
MinimizeManager(ChangeListener cl) {
BOX_1.setStateChangeListener(this);
BOX_2.setStateChangeListener(this);
BOX_3.setStateChangeListener(this);
SAVER = cl;
stateChanged(null);
}
/**
* Notification that a FilterBox has become minimized or restored.
*/
public void stateChanged(ChangeEvent event) {
boolean one, two, three;
one = BOX_1.isMinimized();
two = BOX_2.isMinimized();
three = BOX_3.isMinimized();
// if all three somehow got minimized,
// make the first one restored again.
if(one && two && three) {
BOX_1.setStateChangeListener(null);
BOX_1.restore();
one = false;
BOX_1.setStateChangeListener(this);
}
if(one && two) {
BOX_3.setCanMinimize(false);
} else if(one && three) {
BOX_2.setCanMinimize(false);
} else if(two && three) {
BOX_1.setCanMinimize(false);
} else {
BOX_1.setCanMinimize(true);
BOX_2.setCanMinimize(true);
BOX_3.setCanMinimize(true);
}
removeAll();
add(BOX_1.getComponent());
add(BOX_2.getComponent());
add(BOX_3.getComponent());
invalidate();
revalidate();
repaint();
SAVER.stateChanged(event);
}
}
/**
* A listener for the filter boxes.
*
* Dispatches events based on the source of the selection.
*/
private class SelectorListener implements ListDataListener,
ListSelectionListener, ChangeListener {
/**
* "boolean marker" used to prevent contentChanged method from
* triggering a chain of methods calls which calls itself
* again. <pre>triggered</pre> is set to true in contentsChanged before
* calling updateBoxes, so when it gets called again, it returns
* immediately. Hacky but effective. The variable is reset after the
* call.
*/
private boolean triggered = false;
/**
* Constructs a new SelectorListener for the specified boxes.
*/
SelectorListener() {
BOX_1.addSelectionListener(this, this);
BOX_2.addSelectionListener(this, this);
BOX_3.addSelectionListener(this, this);
BOX_1.setSelectorChangeListener(this);
BOX_2.setSelectorChangeListener(this);
BOX_3.setSelectorChangeListener(this);
}
/**
* Notification that the selectors have changed for a box.
* TODO: Update only the depth that changed.
*/
public void stateChanged(ChangeEvent event) {
String type = RESULTS.getMediaType().getMimeType();
SelectorsHandler.setSelector(type, 0, BOX_1.getSelector());
SelectorsHandler.setSelector(type, 1, BOX_2.getSelector());
SelectorsHandler.setSelector(type, 2, BOX_3.getSelector());
if(event != null) {
FilterBox box = (FilterBox)event.getSource();
setMatchingValues(box);
}
}
/**
* Notification that the selected value has changed.
*
* Updates the selector on the ResultPanel and the list of the
* latter filter boxes.
*
* The event is ignored if the value is adjusting (the mouse is still
* down and moving).
*/
public void valueChanged(ListSelectionEvent event) {
// extraneous events.
if(event.getValueIsAdjusting())
return;
// Because the source of the event is the JList,
// we must figure out which FilterBox it came from
// based on the list of the boxes.
JList source = (JList)event.getSource();
FilterBox box = null;
int depth = -1;
if(source == BOX_1.getList()) {
box = BOX_1;
depth = 1;
} else if(source == BOX_2.getList()) {
box = BOX_2;
depth = 2;
} else if(source == BOX_3.getList()) {
box = BOX_3;
depth = 3;
} else {
throw new IllegalStateException("invalid source: " + source);
}
Selector selector = box.getSelector();
Object value = box.getSelectedValue();
boolean isAll = event.getFirstIndex() == 0 &&
event.getLastIndex() == 0;
// only update the latter boxes only if the selection actually
// changed or we selected 'All'. we must special-case All
// because the filter doesn't change when we go from no selection
// to an All selection, but we want to erase the lower selections.
if(isAll || selectionChanged(selector, value, depth))
updateBoxes(depth, box, true);
}
/**
* Notification that contents of a FilterBox have changed, we are going
* to use this information to update the box above the one in which
* items were added, which will cause this box to the updated as well.
*/
public void contentsChanged(ListDataEvent event) {
if(triggered)
return;
Object source = event.getSource();
FilterBox box = null;
int depth = -1;
if(source == BOX_1.getList().getModel()) {
box = null;
depth = -1;
} else if(source == BOX_2.getList().getModel()) {
box = BOX_1;
depth = 0;
} else if(source == BOX_3.getList().getModel()) {
box = BOX_2;
depth = 1;
} else {
throw new IllegalStateException("invalid source: " + source);
}
if(depth > -1) {
triggered= true;
updateBoxes(depth, box, false);
triggered=false;
}
}
/** Stubbed out method for ListDataListener */
public void intervalRemoved(ListDataEvent e) { }
/** Stubbed out method for ListDataListener */
public void intervalAdded(ListDataEvent e) { }
/**
* If the box to be updated is box1 changes the models of box2 and box3,
* otherwise, if box2 is changed, changes the model of box3. Also
* handles the condition when the the "all" option is selected in any of
* the boxes.
*/
private void updateBoxes(int depth, FilterBox box, boolean clear) {
box.updateTitle();
// If box1 changed, we must change the model of box2 & box3.
// If box2 changed, we only change the model of box3.
Object sel1 = BOX_1.getSelectedValue();
boolean all1 = sel1 == null || MODEL.isAll(sel1);
if(box == BOX_1) {
if(all1)
allPossible(BOX_2);
else
changeModel(BOX_1, BOX_2, sel1);
if(clear)
BOX_2.clearSelection();
}
Object sel2 = BOX_2.getSelectedValue();
boolean all2 = sel2 == null || MODEL.isAll(sel2);
if(box == BOX_2 || box == BOX_1) {
if(all2) {
if(all1)
allPossible(BOX_3);
else
changeModel(BOX_1, BOX_3, sel1);
} else {
changeModel(BOX_2, BOX_3, sel2);
}
if(clear)
BOX_3.clearSelection();
}
}
/**
* Changes the model of the box to be all possible selections for
* this box.
*/
private void allPossible(FilterBox box) {
box.setModel(MODEL.getListModelMap(box.getSelector()));
}
/**
* Changes the model of child to a cross section of the two boxes.
*/
private void changeModel(FilterBox parent, FilterBox child,
Object value) {
child.setModel(
MODEL.getCrossSection(
parent.getModel(),
value,
MODEL.getListModelMap(child.getSelector())
)
);
}
/**
* Creates a new filter to give the ResultPanel.
*/
private boolean selectionChanged(Selector selector,
Object value,
int depth) {
TableLineFilter lineFilter = null;
if(value == null || MODEL.isAll(value))
lineFilter = AllowFilter.instance();
else {
switch(selector.getSelectorType()) {
case Selector.SCHEMA:
lineFilter = new SchemaFilter((NamedMediaType)value);
break;
case Selector.FIELD:
lineFilter = new FieldFilter(selector.getSchema(),
selector.getValue(),
(String)value);
break;
case Selector.PROPERTY:
lineFilter = new PropertyFilter(selector.getValue(),
value);
}
}
return RESULTS.filterChanged(lineFilter, depth);
}
}
}