/*
* FilteredTableModel.java - A Filtered table model decorator
* :tabSize=4:indentSize=4:noTabs=false:
* :folding=explicit:collapseFolds=1:
*
* Copyright (C) 2008 Matthieu Casanova
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.gjt.sp.jedit.gui;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.event.*;
import java.util.*;
/**
* This ListModel delegates another model to add some filtering features to any
* JList.
* To use it you must implement the abstract method passFilter().
* This method is called for each row, and must return true if the row should be
* visible, and false otherwise.
* It is also possible to override the method prepareFilter() that allow you to
* transform the filter String. Usually you can return it as lowercase
* It is not mandatory but highly recommended to give the JList instance to the
* model in order to keep the selection after the filter has been updated
*
* @author Matthieu Casanova
* @version $Id: Buffer.java 8190 2006-12-07 07:58:34Z kpouer $
* @since jEdit 4.3pre11
*/
public abstract class FilteredListModel<E extends ListModel> extends AbstractListModel implements ListDataListener
{
/**
* The delegated table model.
*/
protected E delegated;
private Vector<Integer> filteredIndices;
/**
* This map contains the delegated indices as key and true indices as values.
*/
private Map<Integer, Integer> invertedIndices;
private String filter;
private JList list;
//{{{ FilteredTableModel() constructor
protected FilteredListModel(E delegated)
{
this.delegated = delegated;
delegated.addListDataListener(this);
resetFilter();
} //}}}
//{{{ setList() method
/**
* Set the JList that uses this model.
* It is used to restore the selection after the filter has been applied
* If it is null,
*
* @param list the list that uses the model
*/
public void setList(JList list)
{
if (list.getModel() != this)
throw new IllegalArgumentException("The given list " + list + " doesn't use this model " + this);
this.list = list;
} //}}}
//{{{ getDelegated() method
public E getDelegated()
{
return delegated;
} //}}}
//{{{ setDelegated() method
public void setDelegated(E delegated)
{
this.delegated.removeListDataListener(this);
delegated.addListDataListener(this);
this.delegated = delegated;
} //}}}
//{{{ resetFilter() method
private void resetFilter()
{
filteredIndices = null;
} //}}}
//{{{ setFilter() method
public void setFilter(@Nullable final String filter)
{
Runnable runner = new Runnable()
{
public void run()
{
Set<Integer> selectedIndices = saveSelection();
list.clearSelection();
FilteredListModel.this.filter = filter;
if (filter != null && !filter.isEmpty())
{
int size = delegated.getSize();
String prepped_filter = prepareFilter(filter);
Vector<Integer> indices = new Vector<Integer>(size);
Map<Integer, Integer> invertedIndices = new HashMap<Integer, Integer>();
for (int i = 0; i < size; i++)
{
if (passFilter(i, prepped_filter))
{
Integer delegatedIndice = Integer.valueOf(i);
indices.add(delegatedIndice);
invertedIndices.put(delegatedIndice, indices.size() - 1);
}
}
FilteredListModel.this.invertedIndices = invertedIndices;
filteredIndices = indices;
}
else
resetFilter();
fireContentsChanged(FilteredListModel.this, 0, getSize() - 1);
restoreSelection(selectedIndices);
}
};
SwingUtilities.invokeLater(runner);
} //}}}
//{{{ prepareFilter() method
@Nullable
public String prepareFilter(@Nullable String filter)
{
return filter;
} //}}}
//{{{ passFilter() method
/**
* This callback indicates if a row passes the filter.
*
* @param row the row number the delegate row count
* @param filter the filter string
* @return true if the row must be visible
*/
public abstract boolean passFilter(int row, @Nullable String filter);
//}}}
//{{{ saveSelection()
protected Set<Integer> saveSelection()
{
if (list == null)
return null;
int[] rows = list.getSelectedIndices();
if (rows.length == 0)
return null;
Set<Integer> selectedRows = new HashSet<Integer>(rows.length);
for (int row : rows)
{
selectedRows.add(getTrueRow(row));
}
return selectedRows;
} //}}}
//{{{ restoreSelection() method
protected void restoreSelection(Set<Integer> selectedIndices)
{
if (selectedIndices == null || getSize() == 0)
return;
// To correctly handle "single interval" selection mode,
// each interval has to be selected using a single call to
// setSelectionInterval; calling setSelectionInterval on
// each item cancels the previous selection.
// Sort the list of selected indices to simplify interval
// identification.
Vector<Integer> sel = new Vector<Integer>(selectedIndices);
Collections.sort(sel);
int from = -1;
int to = -1;
for (Integer selectedIndex : sel)
{
int i = getInternal2ExternalRow(selectedIndex.intValue());
if (i != -1)
{
if (from == -1)
from = to = i;
else if (i == to + 1)
to = i;
else
{
list.setSelectionInterval(from, to);
from = to = i;
}
}
}
if (from != -1)
list.setSelectionInterval(from, to);
} //}}}
//{{{ getTrueRow() method
/**
* Converts a row index from the JTable to an internal row index from the delegated model.
*
* @param rowIndex the row index
* @return the row index in the delegated model
*/
public int getTrueRow(int rowIndex)
{
if (filteredIndices == null)
return rowIndex;
return filteredIndices.get(rowIndex).intValue();
} //}}}
//{{{ getInternal2ExternalRow() method
/**
* Converts a row index from the delegated table model into a row index of the JTable.
*
* @param internalRowIndex the internal row index
* @return the table row index or -1 if this row is not visible
*/
public int getInternal2ExternalRow(int internalRowIndex)
{
if (invertedIndices == null)
return internalRowIndex;
Integer externalRowIndex = invertedIndices.get(internalRowIndex);
if (externalRowIndex == null)
return -1;
return externalRowIndex.intValue();
} //}}}
//{{{ getElementAt() method
public Object getElementAt(int index)
{
int trueRowIndex = getTrueRow(index);
return delegated.getElementAt(trueRowIndex);
} //}}}
//{{{ getSize() method
public int getSize()
{
if (filteredIndices == null)
return delegated.getSize();
return filteredIndices.size();
} //}}}
//{{{ contentsChanged() method
public void contentsChanged(ListDataEvent e)
{
setFilter(filter);
} //}}}
//{{{ intervalAdded() method
public void intervalAdded(ListDataEvent e)
{
setFilter(filter);
} //}}}
//{{{ intervalRemoved() method
public void intervalRemoved(ListDataEvent e)
{
setFilter(filter);
} //}}}
}