/*
* Copyright (c) 2008, SQL Power Group Inc.
*
* This file is part of SQL Power Library.
*
* SQL Power Library 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 3 of the License, or
* (at your option) any later version.
*
* SQL Power Library 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, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.swingui.table;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.Timer;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import org.apache.log4j.Logger;
/**
* Searches through a table model using a table text converter. It reduces the visible table
* rows as rows stop matching.
*
* XXX: This model eats tableChanged events that get thrown from below this should be fixed!
*/
public class TableModelSearchDecorator extends AbstractTableModel implements CleanupTableModel, TableModelWrapper {
private static final Logger logger = Logger.getLogger(TableModelSearchDecorator.class);
/**
* We need a way of getting the String value of any cell in the table
* because we need to reliably search for the same text the user sees!
* The Object.toString() often won't match what the table's cell renderers
* put on the screen.
*/
private TableTextConverter tableTextConverter;
private TableModel tableModel;
private List<Integer> rowMapping = null; // null means identity mapping
private Document doc;
private String searchText = null;
/**
* This is a coalescing timed document listener. It does not support
* listening to multiple documents. You must instanciate it for each
* document. You dont need to explicitely add it as it will self
* register a a document listener.
*/
private class TimedDocumentListener implements DocumentListener {
private AtomicBoolean hasUpdates = new AtomicBoolean(false);
private final Timer timer;
private final Document d;
public TimedDocumentListener(Document d) {
this.d = d;
this.timer = new Timer(500, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (hasUpdates.get()) {
hasUpdates.set(false);
search(getSearchText(TimedDocumentListener.this.d));
}
}
});
d.addDocumentListener(this);
this.timer.setInitialDelay(0);
this.timer.setCoalesce(true);
this.timer.setRepeats(true);
this.timer.start();
}
public void cleanup() {
this.timer.stop();
d.removeDocumentListener(this);
}
private String getSearchText(Document e) {
String searchText = null;
try {
searchText = e.getText(0,e.getLength());
} catch (BadLocationException e1) {
throw new RuntimeException(e1);
}
return searchText;
}
public void insertUpdate(DocumentEvent e) {
hasUpdates.set(true);
}
public void removeUpdate(DocumentEvent e) {
hasUpdates.set(true);
}
public void changedUpdate(DocumentEvent e) {
hasUpdates.set(true);
}
};
private TimedDocumentListener docListener = null;
/**
* This table model listener sends events from the model it wraps up to the parent.
*/
final TableModelListener tableModelListener = new TableModelListener() {
public void tableChanged(TableModelEvent e) {
search(searchText);
// XXX adjust co-ordinates to compensate for missing rows (the ones that don't match the search)
fireTableChanged(e);
}
};
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return tableModel.isCellEditable(rowIndex, columnIndex);
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
tableModel.setValueAt(aValue, rowToModel(rowIndex),columnIndex);
}
public TableModelSearchDecorator(TableModel model) {
super();
setWrappedModel(model);
setDoc(new DefaultStyledDocument());
}
private void search(String searchText) {
rowMapping = null;
fireTableDataChanged();
List<Integer> newRowMap = new ArrayList<Integer>();
String[] searchWords = (searchText == null ? null : searchText.split(" "));
synchronized (tableModel) {
for ( int row = 0; row < tableModel.getRowCount(); row++ ) {
boolean match = false;
if ( searchWords == null ) {
match = true;
} else {
int i;
for ( i=0; i<searchWords.length; i++ ) {
match = false;
for ( int column = 0; column < tableModel.getColumnCount(); column++ ) {
Object val = tableModel.getValueAt(row, column);
String value = tableTextConverter.getTextForCell(val);
if ( value.toLowerCase().indexOf(searchWords[i].toLowerCase()) >= 0 ) {
match = true;
if (logger.isDebugEnabled()) {
logger.debug("Match: "+value.toLowerCase()+" contains "+searchWords[i]+ " "+value.toLowerCase().indexOf(searchWords[i].toLowerCase()));
}
break;
}
}
if ( !match )
break;
}
if ( i < searchWords.length )
match = false;
}
if ( match ) {
newRowMap.add(row);
}
}
}
setSearchText(searchText);
rowMapping = newRowMap;
if (logger.isDebugEnabled()) {
logger.debug("new row mapping after search: "+rowMapping);
}
fireTableDataChanged();
}
public int getRowCount() {
if (rowMapping == null) {
return tableModel.getRowCount();
} else {
return rowMapping.size();
}
}
public int getColumnCount() {
return tableModel.getColumnCount();
}
public Object getValueAt(int rowIndex, int columnIndex) {
return tableModel.getValueAt(rowToModel(rowIndex),columnIndex);
}
private int rowToModel(int rowIndex) {
int modelRow = ((rowMapping != null && rowIndex< rowMapping.size()) ? rowMapping.get(rowIndex) : rowIndex);
return modelRow;
}
@Override
public String getColumnName(int column) {
return tableModel.getColumnName(column);
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return tableModel.getColumnClass(columnIndex);
}
public TableModel getWrappedModel() {
return tableModel;
}
public void setWrappedModel(TableModel newModel) {
if (tableModel != null) {
tableModel.removeTableModelListener(tableModelListener);
}
tableModel = newModel;
newModel.addTableModelListener(tableModelListener);
fireTableStructureChanged();
}
public Document getDoc() {
return doc;
}
public void setDoc(Document doc) {
if ( this.doc != null
&& this.docListener != null) {
this.docListener.cleanup();
}
this.doc = doc;
if (doc != null) {
docListener = new TimedDocumentListener(doc);
}
}
public String getSearchText() {
return searchText;
}
public void setSearchText(String searchText) {
this.searchText = searchText;
}
public TableTextConverter getTableTextConverter() {
return tableTextConverter;
}
public void setTableTextConverter(TableTextConverter tableTextConverter) {
this.tableTextConverter = tableTextConverter;
}
public void cleanup() {
docListener.cleanup();
if (tableModel instanceof CleanupTableModel) {
((CleanupTableModel) tableModel).cleanup();
}
}
}