/*
* @(#)TableSearchable.java
*
* Copyright 2002 - 2004 JIDE Software Inc. All rights reserved.
*/
package com.jidesoft.swing;
import com.jidesoft.swing.event.SearchableEvent;
import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
/**
* <code>TableSearchable</code> is an concrete implementation of {@link Searchable} that enables the search function in
* JTable. <p>It's very simple to use it. Assuming you have a JTable, all you need to do is to call
* <code><pre>
* JTable table = ....;
* TableSearchable searchable = new TableSearchable(table);
* </pre></code>
* Now the JTable will have the search function.
* <p/>
* As JTable is a two dimension data, the search is a little different from JList and JTree which both have one
* dimension data. So there is a little work you need to do in order to convert from two dimension data to one dimension
* data. We use the selection mode to determine how to convert. There is a special property called mainIndex. You can
* set it using setMainIndex(). If the JTable is in row selection mode, mainIndex will be the column that you want
* search at. Please note you can change mainIndex at any time.
* <p/>
* On the other hand, if the JTable is in column selection mode, mainIndex will be the row that you want search at.
* There is one more case when cell selection is enabled. In this case, mainIndex will be ignore; all cells will be
* searched.
* <p/>
* In three cases above, the keys for find next and find previous are different too. In row selection mode, up/down
* arrow are the keys. In column selection mode, left/right arrow are keys. In cell selection mode, both up and left
* arrow are keys to find previous occurrence, both down and right arrow are keys to find next occurrence.
* <p/>
* In addition, you might need to override convertElementToString() to provide you own algorithm to do the conversion.
* <code><pre>
* JTable table = ....;
* TableSearchable searchable = new TableSearchable(table) {
* protected String convertElementToString(Object object) {
* ...
* }
* };
* </pre></code>
* <p/>
* Additional customization can be done on the base Searchable class such as background and foreground color,
* keystrokes, case sensitivity,
*/
public class TableSearchable extends Searchable implements TableModelListener, PropertyChangeListener {
private int[] _searchColumnIndices = {0};
public TableSearchable(JTable table) {
super(table);
}
@Override
public void installListeners() {
super.installListeners();
if (_component instanceof JTable) {
((JTable) _component).getModel().addTableModelListener(this);
_component.addPropertyChangeListener("model", this);
}
}
@Override
public void uninstallListeners() {
super.uninstallListeners();
if (_component instanceof JTable) {
((JTable) _component).getModel().removeTableModelListener(this);
_component.removePropertyChangeListener("model", this);
}
}
@Override
protected void setSelectedIndex(int index, boolean incremental) {
int majorIndex, minorIndex;
JTable table = ((JTable) _component);
if (isColumnSelectionAllowed(table)) {
minorIndex = index;
majorIndex = getMainIndex();
addTableSelection(table, majorIndex, minorIndex, incremental);
}
else if (isRowSelectionAllowed(table)) {
majorIndex = index;
minorIndex = table.convertColumnIndexToView(getMainIndex());
addTableSelection(table, majorIndex, minorIndex, incremental);
}
else { // cell selection allowed
int columnCount = table.getColumnCount();
if (columnCount == 0) {
return;
}
majorIndex = index / columnCount;
minorIndex = index % columnCount;
addTableSelection(table, majorIndex, minorIndex, incremental);
}
}
/**
* Selects the cell at the specified row and column index. If incremental is true, the previous selection will not
* be cleared. This method will use {@link JTable#changeSelection(int,int,boolean,boolean)} method to select the
* cell if the row and column index is in the range and the cell was not selected. The last two parameters of
* changeSelection is true and false respectively.
*
* @param table the table
* @param rowIndex the row index of the cell.
* @param columnIndex the column index of the cell
* @param incremental false to clear all previous selection. True to keep the previous selection.
*/
protected void addTableSelection(JTable table, int rowIndex, int columnIndex, boolean incremental) {
if (!incremental)
table.clearSelection();
if (rowIndex >= 0 && columnIndex >= 0 && rowIndex < table.getRowCount() && columnIndex < table.getColumnCount()
&& !table.isCellSelected(rowIndex, columnIndex)) {
table.changeSelection(rowIndex, columnIndex, true, false);
}
}
/**
* Is the column selection allowed?
*
* @param table the table.
* @return true if the table is the column selection.
*/
protected boolean isColumnSelectionAllowed(JTable table) {
// NOTES: must sync with TableShrinkSearchableSupport#isColumnSelectionAllowed.
return getSearchColumnIndices().length == 1 && (table.getColumnSelectionAllowed() && !table.getRowSelectionAllowed());
}
/**
* Is the row selection allowed?
*
* @param table the table.
* @return true if the table is the row selection.
*/
protected boolean isRowSelectionAllowed(JTable table) {
// NOTES: must sync with TableShrinkSearchableSupport#isRowSelectionAllowed.
return getSearchColumnIndices().length == 1 && (!table.getColumnSelectionAllowed() && table.getRowSelectionAllowed());
}
/**
* Are we trying to search on multi-columns (but NOT all columns)?
*
* @return true if the search is set to look at multi-columns (but NOT all columns).
*/
protected boolean isSearchSelectedRows() {
return getSearchColumnIndices().length > 1;
}
/**
* Gets the selected index.
*
* @return the selected index.
*/
@Override
protected int getSelectedIndex() {
JTable table = (JTable) _component;
if (isColumnSelectionAllowed(table)) {
return table.getColumnModel().getSelectionModel().getAnchorSelectionIndex();
}
else if (isRowSelectionAllowed(table)) {
return table.getSelectionModel().getAnchorSelectionIndex();
}
else { // cell selection allowed
return table.getSelectionModel().getAnchorSelectionIndex() * table.getColumnCount() + table.getColumnModel().getSelectionModel().getAnchorSelectionIndex();
}
}
@Override
protected Object getElementAt(int index) {
JTable table = (JTable) _component;
if (isColumnSelectionAllowed(table)) { // column selection mode
return getValueAt(table, getMainIndex(), index);
}
else if (isRowSelectionAllowed(table)) { // row selection mode
return getValueAt(table, index, table.convertColumnIndexToView(getMainIndex()));
}
else if (isSearchSelectedRows()) { // search on multi columns
int columnIndex = index % table.getColumnCount();
int modelIndex = table.convertColumnIndexToModel(columnIndex);
boolean doNotSearch = true;
for (int i : getSearchColumnIndices()) {
if (i == modelIndex) {
doNotSearch = false;
}
}
if (doNotSearch) {
return null;
}
int rowIndex = index / table.getColumnCount();
return getValueAt(table, rowIndex, columnIndex);
}
else { // cell selection allowed
int columnIndex = index % table.getColumnCount();
int rowIndex = index / table.getColumnCount();
return getValueAt(table, rowIndex, columnIndex);
}
}
/**
* Get string value of the table.
*
* @param table the JTable
* @param rowIndex the row index
* @param columnIndex the column index
* @return the string value of the cell in the table.
*/
protected Object getValueAt(JTable table, int rowIndex, int columnIndex) {
if (rowIndex >= 0 && rowIndex < table.getRowCount() && columnIndex >= 0 && columnIndex < table.getColumnCount()) {
return table.getValueAt(rowIndex, columnIndex);
}
else {
return null;
}
}
@Override
protected int getElementCount() {
JTable table = ((JTable) _component);
if (isColumnSelectionAllowed(table)) {
return table.getColumnCount();
}
else if (isRowSelectionAllowed(table)) {
return table.getRowCount();
}
else { // cell selection allowed
return table.getColumnCount() * table.getRowCount();
}
}
@Override
protected String convertElementToString(Object item) {
if (item != null) {
return item.toString();
}
else {
return "";
}
}
/**
* Gets the indexes of the column to be searched.
*
* @return the indexes of the column to be searched.
*/
public int[] getSearchColumnIndices() {
return _searchColumnIndices;
}
/**
* Gets the index of the column to be searched.
*
* @return the index of the column to be searched.
*/
public int getMainIndex() {
if (_searchColumnIndices.length == 0) {
return -1;
}
return _searchColumnIndices[0];
}
/**
* Sets the main indexes. Main indexes are the columns index which you want to be searched.
*
* @param columnIndices the index of the columns to be searched. If empty, all columns will be searched.
*/
public void setSearchColumnIndices(int[] columnIndices) {
if (columnIndices == null) {
columnIndices = new int[0];
}
int[] old = _searchColumnIndices;
if (!JideSwingUtilities.equals(old, columnIndices, true)) {
_searchColumnIndices = columnIndices;
hidePopup();
}
}
/**
* Sets the main index. Main index is the column index which you want to be searched.
*
* @param mainIndex the index of the column to be searched. If -1, all columns will be searched.
*/
public void setMainIndex(int mainIndex) {
int[] temp = {mainIndex};
if (mainIndex < 0) {
temp = new int[0];
}
int[] old = _searchColumnIndices;
if (old != temp) {
_searchColumnIndices = temp;
hidePopup();
}
}
@Override
protected boolean isFindNextKey(KeyEvent e) {
int keyCode = e.getKeyCode();
JTable table = ((JTable) _component);
if (isColumnSelectionAllowed(table)) {
return keyCode == KeyEvent.VK_RIGHT;
}
else if (isRowSelectionAllowed(table)) {
return keyCode == KeyEvent.VK_DOWN;
}
else { // cell selection allowed
return keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_RIGHT;
}
}
@Override
protected boolean isFindPreviousKey(KeyEvent e) {
int keyCode = e.getKeyCode();
JTable table = ((JTable) _component);
if (isColumnSelectionAllowed(table)) {
return keyCode == KeyEvent.VK_LEFT;
}
else if (isRowSelectionAllowed(table)) {
return keyCode == KeyEvent.VK_UP;
}
else { // cell selection allowed
return keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_LEFT;
}
}
public void tableChanged(TableModelEvent e) {
if (isProcessModelChangeEvent()) {
hidePopup();
fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_MODEL_CHANGE));
}
}
public void propertyChange(PropertyChangeEvent evt) {
if ("model".equals(evt.getPropertyName())) {
hidePopup();
if (evt.getOldValue() instanceof TableModel) {
((TableModel) evt.getOldValue()).removeTableModelListener(this);
}
if (evt.getNewValue() instanceof TableModel) {
((TableModel) evt.getNewValue()).addTableModelListener(this);
}
fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_MODEL_CHANGE));
}
}
@Override
protected boolean isActivateKey(KeyEvent e) {
boolean editable = isSelectedCellEditable();
return !editable && super.isActivateKey(e);
}
/**
* Checks if the selected cell is editable. If yes, we will not activate Searchable when key is typed.
*
* @return true if the selected cell is editable.
*/
protected boolean isSelectedCellEditable() {
int selectedRow = ((JTable) _component).getSelectedRow();
int selectedColumn = ((JTable) _component).getSelectedColumn();
return selectedRow != -1 && selectedColumn != -1 && ((JTable) _component).isCellEditable(selectedRow, selectedColumn);
}
}