/*
* FilteredTableModel.java - A Filtered table model decorator
* :tabSize=4:indentSize=4:noTabs=false:
* :folding=explicit:collapseFolds=1:
*
* Copyright (C) 2007 Shlomy Reinstein
* Copyright (C) 2007 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.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
import java.util.*;
/**
* This TableModel delegates another model to add some filtering features to any
* JTable.
* 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
* <p>
* Here is an example of how to use it extracted from the InstallPanel
* <code>
* PluginTableModel tableModel = new PluginTableModel();
* filteredTableModel = new FilteredTableModel<PluginTableModel>(tableModel)
* {
* public String prepareFilter(String filter)
* {
* return filter.toLowerCase();
* }
* public boolean passFilter(int row, String filter)
* {
* String pluginName = (String) delegated.getValueAt(row, 1);
* return pluginName.toLowerCase().contains(filter);
* }
* };
* table = new JTable(filteredTableModel);
* filteredTableModel.setTable(table);
* </code>
* It is not mandatory but highly recommended to give the JTable instance to the
* model in order to keep the selection after the filter has been updated
*
* @author Shlomy Reinstein
* @author Matthieu Casanova
* @version $Id: Buffer.java 8190 2006-12-07 07:58:34Z kpouer $
* @since jEdit 4.3pre11
*/
public abstract class FilteredTableModel<E extends TableModel> extends AbstractTableModel implements TableModelListener
{
/**
* 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 JTable table;
//{{{ FilteredTableModel() constructors
protected FilteredTableModel(E delegated)
{
this.delegated = delegated;
delegated.addTableModelListener(this);
resetFilter();
}
protected FilteredTableModel()
{
} //}}}
//{{{ setTable() method
/**
* Set the JTable that uses this model.
* It is used to restore the selection after the filter has been applied
* If it is null,
*
* @param table the table that uses the model
*/
public void setTable(JTable table)
{
if (table.getModel() != this)
throw new IllegalArgumentException("The given table " + table + " doesn't use this model " + this);
this.table = table;
} //}}}
//{{{ getDelegated() method
public E getDelegated()
{
return delegated;
} //}}}
//{{{ setDelegated() method
public void setDelegated(E delegated)
{
if (this.delegated != null)
this.delegated.removeTableModelListener(this);
delegated.addTableModelListener(this);
this.delegated = delegated;
fireTableStructureChanged();
} //}}}
//{{{ resetFilter() method
private void resetFilter()
{
filteredIndices = null;
} //}}}
//{{{ setFilter() method
public void setFilter(String filter)
{
Set<Integer> selectedIndices = saveSelection();
this.filter = filter;
if (filter != null && !filter.isEmpty())
{
int size = delegated.getRowCount();
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, filter))
{
Integer delegatedIndice = Integer.valueOf(i);
indices.add(delegatedIndice);
invertedIndices.put(delegatedIndice, indices.size() - 1);
}
}
this.invertedIndices = invertedIndices;
filteredIndices = indices;
}
else
resetFilter();
fireTableDataChanged();
restoreSelection(selectedIndices);
} //}}}
//{{{ prepareFilter() method
public String prepareFilter(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, String filter);
//}}}
//{{{ saveSelection()
private Set<Integer> saveSelection()
{
if (table == null)
return null;
int[] rows = table.getSelectedRows();
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
private void restoreSelection(Set<Integer> selectedIndices)
{
if (selectedIndices == null || getRowCount() == 0)
return;
for (Integer selectedIndex : selectedIndices)
{
int i = getInternal2ExternalRow(selectedIndex.intValue());
if (i != -1)
table.getSelectionModel().setSelectionInterval(i, i);
}
} //}}}
//{{{ getRowCount() method
@Override
public int getRowCount()
{
if (filteredIndices == null)
return delegated.getRowCount();
return filteredIndices.size();
} //}}}
//{{{ getColumnCount() method
@Override
public int getColumnCount()
{
return delegated.getColumnCount();
} //}}}
//{{{ getColumnName() method
@Override
public String getColumnName(int columnIndex)
{
return delegated.getColumnName(columnIndex);
} //}}}
//{{{ getColumnClass() method
@Override
public Class<?> getColumnClass(int columnIndex)
{
return delegated.getColumnClass(columnIndex);
} //}}}
//{{{ isCellEditable() method
@Override
public boolean isCellEditable(int rowIndex, int columnIndex)
{
int trueRowIndex = getTrueRow(rowIndex);
return delegated.isCellEditable(trueRowIndex, columnIndex);
} //}}}
//{{{ getValueAt() method
@Override
public Object getValueAt(int rowIndex, int columnIndex)
{
int trueRowIndex = getTrueRow(rowIndex);
return delegated.getValueAt(trueRowIndex, columnIndex);
} //}}}
//{{{ setValueAt() method
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex)
{
int trueRowIndex = getTrueRow(rowIndex);
delegated.setValueAt(aValue, trueRowIndex, columnIndex);
} //}}}
//{{{ 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();
} //}}}
/**
* This fine grain notification tells listeners the exact range
* of cells, rows, or columns that changed.
*/
@Override
public void tableChanged(TableModelEvent e)
{
setFilter(filter);
}
}