/*
* Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved.
*
* This file is part of the Jspresso framework.
*
* Jspresso is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Jspresso 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Jspresso. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jspresso.framework.view.swing;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import org.jspresso.framework.util.gui.IIndexMapper;
/**
* AbstractTableSorter is the base class for TableModels sortable decorators;
* adding sorting functionality to a supplied TableModel.
* <p/>
* When the tableHeader property is set, either by using the setTableHeader()
* method or the two argument constructor, the table header may be used as a
* complete UI for TableSorter. The default renderer of the tableHeader is
* decorated with a renderer that indicates the sorting status of each column.
* In addition, a mouse listener is installed with the following behavior:
* <ul>
* <li>Mouse-click: Clears the sorting status of all other columns and advances
* the sorting status of that column through three values: {NOT_SORTED,
* ASCENDING, DESCENDING} (then back to NOT_SORTED again).
* <li>SHIFT-mouse-click: Clears the sorting status of all other columns and
* cycles the sorting status of the column through the same three values, in the
* opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
* <li>CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except that
* the changes to the column do not cancel the statuses of columns that are
* already sorting - giving a way to initiate a compound sort.
* </ul>
* <p/>
* This is an split from the TableSorter class.
*
* @author Philip Milne
* @author Brendon McLean
* @author Dan van Enckevort
* @author Parwinder Sekhon
* @author Vincent Vandenschrick
* @version 2.0 02/27/04
*/
public abstract class AbstractTableSorter extends AbstractTableModel implements
IIndexMapper {
/**
* {@code ASCENDING}.
*/
public static final int ASCENDING = 1;
/**
* {@code DESCENDING}.
*/
public static final int DESCENDING = -1;
/**
* {@code NOT_SORTED}.
*/
public static final int NOT_SORTED = 0;
private static final Directive NOT_SORTED_DIRECTIVE = new Directive(-1,
NOT_SORTED);
private static final long serialVersionUID = 7759053241235858224L;
private Icon downIcon;
private final MouseListener mouseListener;
private final List<Directive> sortingColumns;
private JTableHeader tableHeader;
private TableModel tableModel;
private final TableModelListener tableModelListener;
private Icon upIcon;
/**
* Constructs a new {@code AbstractTableSorter} instance.
*
* @param tableModel
* tableModel.
* @param tableHeader
* tableHeader.
*/
public AbstractTableSorter(TableModel tableModel, JTableHeader tableHeader) {
this.sortingColumns = new ArrayList<>();
this.mouseListener = new MouseHandler();
this.tableModelListener = createTableModelHandler();
setTableHeader(tableHeader);
setTableModel(tableModel);
}
/**
* {@inheritDoc}
*/
@Override
public Class<?> getColumnClass(int column) {
return tableModel.getColumnClass(column);
}
/**
* {@inheritDoc}
*/
@Override
public int getColumnCount() {
if (tableModel == null) {
return 0;
}
return tableModel.getColumnCount();
}
/**
* {@inheritDoc}
*/
@Override
public String getColumnName(int column) {
return tableModel.getColumnName(column);
}
/**
* {@inheritDoc}
*/
@Override
public int getRowCount() {
if (tableModel == null) {
return 0;
}
return tableModel.getRowCount();
}
/**
* Gets sorting status.
*
* @param column
* column.
* @return sorting status.
*/
public int getSortingStatus(int column) {
return getDirective(column).direction;
}
/**
* Gets tableHeader.
*
* @return tableHeader.
*/
public JTableHeader getTableHeader() {
return tableHeader;
}
/**
* Gets tableModel.
*
* @return tableModel.
*/
public TableModel getTableModel() {
return tableModel;
}
/**
* {@inheritDoc}
*/
@Override
public Object getValueAt(int row, int column) {
return tableModel.getValueAt(modelIndex(row), column);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isCellEditable(int row, int column) {
return tableModel.isCellEditable(modelIndex(row), column);
}
/**
* Tests whether it is sorting.
*
* @return true if sorting
*/
public boolean isSorting() {
return sortingColumns.size() != 0;
}
/**
* Sets the downIcon.
*
* @param downIcon
* the downIcon to set.
*/
public void setDownIcon(Icon downIcon) {
this.downIcon = downIcon;
}
/**
* Sets column sorting status.
*
* @param column
* column.
* @param status
* status.
*/
public void setSortingStatus(int column, int status) {
Directive directive = getDirective(column);
if (directive != NOT_SORTED_DIRECTIVE) {
sortingColumns.remove(directive);
}
if (status != NOT_SORTED) {
sortingColumns.add(new Directive(column, status));
}
sortingStatusChanged();
}
/**
* Sets tableHeader.
*
* @param tableHeader
* tableHeader.
*/
public void setTableHeader(JTableHeader tableHeader) {
if (this.tableHeader != null) {
this.tableHeader.removeMouseListener(mouseListener);
TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer();
if (defaultRenderer instanceof SortableHeaderRenderer) {
this.tableHeader
.setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).delegate);
}
}
this.tableHeader = tableHeader;
if (this.tableHeader != null) {
this.tableHeader.addMouseListener(mouseListener);
this.tableHeader.setDefaultRenderer(new SortableHeaderRenderer(this,
this.tableHeader.getDefaultRenderer()));
}
}
/**
* Sets tableModel.
*
* @param tableModel
* tableModel.
*/
public void setTableModel(TableModel tableModel) {
if (this.tableModel != null && tableModelListener != null) {
this.tableModel.removeTableModelListener(tableModelListener);
}
this.tableModel = tableModel;
if (this.tableModel != null && tableModelListener != null) {
this.tableModel.addTableModelListener(tableModelListener);
}
clearSortingState();
fireTableStructureChanged();
}
/**
* Sets the upIcon.
*
* @param upIcon
* the upIcon to set.
*/
public void setUpIcon(Icon upIcon) {
this.upIcon = upIcon;
}
/**
* {@inheritDoc}
*/
@Override
public void setValueAt(Object aValue, int row, int column) {
tableModel.setValueAt(aValue, modelIndex(row), column);
}
/**
* Cancels sorting.
*/
protected void cancelSorting() {
sortingColumns.clear();
// sortingStatusChanged();
}
/**
* Performs any operation needed to clear some internal state when sorting has
* changed.
*/
protected void clearSortingState() {
// NO-OP by default.
}
/**
* Retrieves the column view index from the model index.
*
* @param modelColumnIndex
* the model column index.
* @return the column index.
*/
protected int convertColumnIndexToView(int modelColumnIndex) {
if (modelColumnIndex < 0) {
return modelColumnIndex;
}
TableColumnModel cm = getTableHeader().getColumnModel();
for (int column = 0; column < getColumnCount(); column++) {
if (cm.getColumn(column).getModelIndex() == modelColumnIndex) {
return column;
}
}
return -1;
}
/**
* Creates a table model listener to react to the underlying table model
* change events.
*
* @return the table model listener.
*/
protected abstract TableModelListener createTableModelHandler();
/**
* Gets HeaderRendererIcon.
*
* @param column
* column.
* @return HeaderRendererIcon
*/
public Icon getHeaderRendererIcon(int column) {
Directive directive = getDirective(column);
if (directive == NOT_SORTED_DIRECTIVE) {
return null;
}
if (directive.direction == DESCENDING) {
if (downIcon != null) {
return downIcon;
}
} else {
if (upIcon != null) {
return upIcon;
}
}
return null;
}
/**
* Gets the sortingColumns.
*
* @return the sortingColumns.
*/
protected List<Directive> getSortingColumns() {
return sortingColumns;
}
/**
* Whether the table column is sortable.
*
* @param column
* the table column to test.
* @return true is the table column is sortable.
*/
protected boolean isSortable(TableColumn column) {
return column.getIdentifier() != null
&& column.getIdentifier().toString().length() > 0
&& !column.getIdentifier().toString().startsWith("#");
}
/**
* This method is triggered whenever the user clicks changed the sorting in
* any way.
*/
protected abstract void sortingStatusChanged();
private Directive getDirective(int column) {
for (Directive directive : sortingColumns) {
if (directive.column == column) {
return directive;
}
}
return NOT_SORTED_DIRECTIVE;
}
/**
* Internal class to represent a sorted column state.
*/
protected static final class Directive {
private final int column;
private final int direction;
/**
* Constructs a new {@code Directive} instance.
*
* @param column the column index.
* @param direction the direction.
*/
private Directive(int column, int direction) {
this.column = column;
this.direction = direction;
}
/**
* Gets the column.
*
* @return the column.
*/
protected int getColumn() {
return column;
}
/**
* Gets the direction.
*
* @return the direction.
*/
protected int getDirection() {
return direction;
}
}
private class MouseHandler extends MouseAdapter {
/**
* {@inheritDoc}
*/
@Override
public void mouseClicked(MouseEvent e) {
JTableHeader h = (JTableHeader) e.getSource();
TableColumnModel columnModel = h.getColumnModel();
int columnViewIndex = columnModel.getColumnIndexAtX(e.getX());
TableColumn column = columnModel.getColumn(columnViewIndex);
if (!isSortable(column)) {
return;
}
int columnModelIndex = column.getModelIndex();
if (columnModelIndex != -1) {
int status = getSortingStatus(columnModelIndex);
if (!e.isControlDown()) {
cancelSorting();
}
// Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING}
// or
// {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is
// pressed.
if (e.isShiftDown()) {
status -= 1;
} else {
status += 1;
}
status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, 1}
setSortingStatus(columnModelIndex, status);
}
}
}
/**
* Decorates a table cell renderer with sorting state.
*
* @author Vincent Vandenschrick
*/
public static class SortableHeaderRenderer implements TableCellRenderer {
private final AbstractTableSorter tableSorter;
private final TableCellRenderer delegate;
/**
* Constructs a new {@code SortableHeaderRenderer} instance.
*
* @param tableSorter
* the table sorter used to compute the sort icon.
* @param delegate
* the wrapped table cell renderer.
*/
public SortableHeaderRenderer(AbstractTableSorter tableSorter,
TableCellRenderer delegate) {
this.tableSorter = tableSorter;
this.delegate = delegate;
}
/**
* {@inheritDoc}
*/
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
Component rendererComponent = delegate.getTableCellRendererComponent(
table, value, isSelected, hasFocus, row, column);
int modelColumn = table.convertColumnIndexToModel(column);
Icon sortIcon = tableSorter.getHeaderRendererIcon(modelColumn);
if (sortIcon != null) {
JLabel compoundRenderer = new JLabel();
compoundRenderer.setLayout(new BoxLayout(compoundRenderer,
BoxLayout.X_AXIS));
JLabel sortIconLabel = new JLabel(sortIcon);
compoundRenderer.add(rendererComponent);
compoundRenderer.add(Box.createRigidArea(new Dimension(5, 0)));
compoundRenderer.add(sortIconLabel);
rendererComponent = compoundRenderer;
}
return rendererComponent;
}
}
}