/* * 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); } //}}} }