/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Tiny Look and Feel *
* *
* (C) Copyright 2003 - 2007 Hans Bickel *
* *
* For licensing information and credits, please refer to the *
* comment in file de.muntjak.tinylookandfeel.TinyLookAndFeel *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
package de.muntjak.tinylookandfeel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.*;
import de.muntjak.tinylookandfeel.controlpanel.*;
import de.muntjak.tinylookandfeel.table.SortableTableData;
import de.muntjak.tinylookandfeel.table.TinyTableHeaderRenderer;
/**
* TinyTableHeaderUI
*
* @version 1.0
* @author Hans Bickel
*/
public class TinyTableHeaderUI extends BasicTableHeaderUI {
/** Client property key */
public static final String ROLLOVER_COLUMN_KEY = "rolloverColumn";
/** Client property key */
public static final String SORTED_COLUMN_KEY = "sortedColumn";
/** Client property key */
public static final String SORTING_DIRECTION_KEY = "sortingDirection";
// Action command constants
private static final int ADD_COLUMN = 0;
private static final int REMOVE_COLUMN = 1;
private static final int REPLACE_COLUMN = 2;
/** The horizontal distance the mouse must move to
* be recognized as a mouse drag. */
private static final int MINIMUM_DRAG_DISTANCE = 5;
private static final HashMap sortingCache = new HashMap();
protected SortableTableHandler handler;
protected TinyTableHeaderRenderer headerRenderer;
public TinyTableHeaderUI() {
super();
}
public static ComponentUI createUI(JComponent header) {
return new TinyTableHeaderUI();
}
protected void installListeners() {
super.installListeners();
// new in 1.3.6
// install our own renderer so we can realize rollovers
// and arrow icons (sortable table models only)
headerRenderer = new TinyTableHeaderRenderer();
header.setDefaultRenderer(headerRenderer);
// new in 1.3.6
// support for sortable table models
handler = new SortableTableHandler();
header.addMouseListener(handler);
header.addMouseMotionListener(handler);
header.getColumnModel().addColumnModelListener(handler);
SortingInformation sortingInfo = (SortingInformation)sortingCache.get(header);
if(sortingInfo != null) {
handler.restoreSortingInformation(header, sortingInfo);
}
}
protected void uninstallListeners() {
super.uninstallListeners();
// Remove sorting information - even if we only
// switch TinyLaF themes, we cannot preserve state
handler.removeSortingInformation();
header.removeMouseListener(handler);
header.removeMouseMotionListener(handler);
header.getColumnModel().removeColumnModelListener(handler);
// if(header.getTable() != null) {
// TableModel model = header.getTable().getModel();
//
// if(model != null) {
// model.removeTableModelListener(handler);
// }
// }
}
public Dimension getPreferredSize(JComponent c) {
// this is for the very special case that the header value
// of the 1st column is an empty string and no custom header
// renderers were defined.
// In this case, the dimension returned by super.getPreferredSize()
// has a height of 2 but that's not what we want...
Dimension d = super.getPreferredSize(c);
d.height = Math.max(16, d.height);
return d;
}
/**
* Call this method to programmatically initiate sorting on (sortable)
* table models. Especially if your data is sorted by default, you
* should call this method before the table is displayed the first
* time.
* @param columns array of column indices sorted by priority (highest
* priority first)
* @param sortingDirections array containing the sorting direction for
* each sorted column. Values are either
* <ul>
* <li><code>de.muntjak.tinylookandfeel.table.SortableTableData.SORT_ASCENDING</code> or
* <li><code>de.muntjak.tinylookandfeel.table.SortableTableData.SORT_DESCENDING</code>
* </ul>
*
* @param table the table displaying the data
* @throws IllegalArgumentException If any of the arguments is
* <code>null</code> or arguments <code>colums</code> and
* <code>sortingDirections</code> are of different length
*/
public void sortColumns(int[] columns, int[] sortingDirections, JTable table) {
if(handler == null) return;
handler.sortColumns(columns, sortingDirections, table);
}
/**
* Sets horizontal alignments of table header renderers where an index
* in the argument array corresponds to a column index.
* <br>
* Note: If the length of the argument array is less than the number
* of columns, unspecified columns default to <code>CENTER</code> alignment.
* If the length of the argument array is greater than the number
* of columns, surplus information will be ignored.
* @param alignments array of the following constants
* defined in <code>SwingConstants</code>:
* <code>LEFT</code>,
* <code>CENTER</code>,
* <code>RIGHT</code>,
* <code>LEADING</code> or
* <code>TRAILING</code>.
*/
public void setHorizontalAlignments(int[] alignments) {
if(headerRenderer == null) return;
headerRenderer.setHorizontalAlignments(alignments);
}
/**
* A data container used to store sorting information
* between instantiations (LAF change)
* @author Hans Bickel
*
*/
private class SortingInformation {
private Vector sortedViewColumns;
private Vector sortedModelColumns;
private Vector sortingDirections;
SortingInformation(
Vector sortedViewColumns,
Vector sortedModelColumns,
Vector sortingDirections)
{
this.sortedViewColumns = sortedViewColumns;
this.sortedModelColumns = sortedModelColumns;
this.sortingDirections = sortingDirections;
}
}
/**
* This handler is for table headers whose table model
* implements SortableTableData. It is attached to the header
* in createUI(JComponent) and keeps track of the sorting
* direction and the sorted column.
*
* @author Hans Bickel
* @since 1.3.6
*
*/
private class SortableTableHandler implements MouseListener,
MouseMotionListener, TableColumnModelListener
{
// -1 means that no column is currently a rollover candidate,
// else it is the index of the rollover column
private int rolloverColumn = -1;
private int pressedColumn = -1;
private Vector sortedViewColumns = new Vector();
private Vector sortedModelColumns = new Vector();
private Vector sortingDirections = new Vector();
private boolean mouseInside = false;
private boolean mouseDragged = false;
private boolean inDrag = false;
private Point pressedPoint;
void sortColumns(int[] columns, int[] directions, JTable table) {
if(columns == null) {
throw new IllegalArgumentException("columns argument may not be null");
}
if(directions == null) {
throw new IllegalArgumentException("directions argument may not be null");
}
if(columns.length != directions.length) {
throw new IllegalArgumentException("columns argument and directions argument must be of equal length");
}
if(columns.length > table.getColumnCount()) {
throw new IllegalArgumentException("Length of columns argument is greater than number of table columns");
}
JTableHeader header = table.getTableHeader();
SortableTableData model = getTableModel(header);
if(model == null) return;
sortedViewColumns.clear();
sortedModelColumns.clear();
sortingDirections.clear();
for(int i = 0; i < columns.length; i++) {
sortedViewColumns.add(new Integer(columns[i]));
sortedModelColumns.add(new Integer(getModelColumn(header, columns[i])));
sortingDirections.add(new Integer(directions[i]));
}
header.putClientProperty(SORTED_COLUMN_KEY, vectorToIntArray(sortedViewColumns));
header.putClientProperty(SORTING_DIRECTION_KEY, vectorToIntArray(sortingDirections));
model.sortColumns(
vectorToIntArray(sortedModelColumns),
vectorToIntArray(sortingDirections),
table);
header.repaint();
}
// MouseListener implementation
public void mouseEntered(MouseEvent e) {
mouseInside = true;
if(mouseDragged) return;
SortableTableData model = getTableModel(e.getSource());
if(model == null) return;
JTableHeader header = (JTableHeader)e.getSource();
int viewColumn = header.columnAtPoint(e.getPoint());
int modelColumn = getModelColumnAt(e);
if(!model.isColumnSortable(modelColumn)) {
if(rolloverColumn != -1) {
rolloverColumn = -1;
header.putClientProperty(ROLLOVER_COLUMN_KEY, null);
}
}
else {
rolloverColumn = viewColumn;
header.putClientProperty(ROLLOVER_COLUMN_KEY, new Integer(rolloverColumn));
}
header.repaint();
}
public void mouseExited(MouseEvent e) {
mouseInside = false;
JTableHeader header = (JTableHeader)e.getSource();
if(inDrag && header.getReorderingAllowed()) return;
SortableTableData model = getTableModel(e.getSource());
if(model == null) return;
if(rolloverColumn != -1) {
rolloverColumn = -1;
header.putClientProperty(ROLLOVER_COLUMN_KEY, null);
header.repaint();
}
}
public void mouseReleased(MouseEvent e) {
inDrag = false;
if(e.isPopupTrigger()) {
// showHeaderPopup(e);
return;
}
if(!mouseInside) {
mouseDragged = false;
return;
}
SortableTableData model = getTableModel(e.getSource());
if(model == null) {
mouseDragged = false;
return;
}
JTableHeader header = (JTableHeader)e.getSource();
int viewColumn = header.columnAtPoint(e.getPoint());
if(viewColumn == -1) {
mouseDragged = false;
return;
}
int modelColumn = getModelColumnAt(e);
if(!model.isColumnSortable(modelColumn)) {
if(rolloverColumn != -1) {
rolloverColumn = -1;
header.putClientProperty(ROLLOVER_COLUMN_KEY, null);
}
}
else {
rolloverColumn = viewColumn;
header.putClientProperty(ROLLOVER_COLUMN_KEY, new Integer(rolloverColumn));
}
if(mouseDragged) {
mouseDragged = false;
return;
}
if(!model.isColumnSortable(modelColumn)) return;
if(pressedColumn != viewColumn) return;
// for header renderer, sorted column must be viewColumn
Integer vc = new Integer(viewColumn);
if(sortedViewColumns.contains(vc)) {
int index = sortedViewColumns.indexOf(vc);
if(e.isAltDown()) {
// remove clicked column from sorted columns
sortedViewColumns.remove(index);
sortedModelColumns.remove(index);
sortingDirections.remove(index);
}
else if((e.isControlDown() && model.supportsMultiColumnSort()) ||
sortedModelColumns.size() == 1)
{
// change sorting direction of clicked column
int sortingDirection = ((Integer)sortingDirections.get(index)).intValue();
if(sortingDirection != SortableTableData.SORT_DESCENDING) {
sortingDirection = SortableTableData.SORT_DESCENDING;
}
else {
sortingDirection = SortableTableData.SORT_ASCENDING;
}
sortingDirections.remove(index);
sortingDirections.add(index, new Integer(sortingDirection));
}
else {
// change sorting direction of clicked column
int sortingDirection = ((Integer)sortingDirections.get(index)).intValue();
if(sortingDirection != SortableTableData.SORT_DESCENDING) {
sortingDirection = SortableTableData.SORT_DESCENDING;
}
else {
sortingDirection = SortableTableData.SORT_ASCENDING;
}
// remove all sorted columns and initialize
// sorted columns with clicked column
sortedViewColumns.clear();
sortedModelColumns.clear();
sortingDirections.clear();
sortedViewColumns.add(vc);
sortedModelColumns.add(new Integer(getModelColumn(e, viewColumn)));
sortingDirections.add(new Integer(sortingDirection));
}
}
else {
if(e.isAltDown()) {
// ignore
return;
}
else if(e.isControlDown() && model.supportsMultiColumnSort()) {
// add clicked column to sorted columns
sortedViewColumns.add(vc);
sortedModelColumns.add(new Integer(getModelColumn(e, viewColumn)));
sortingDirections.add(new Integer(SortableTableData.SORT_ASCENDING));
}
else {
// initialize sorted columns with clicked column
sortedViewColumns.clear();
sortedModelColumns.clear();
sortingDirections.clear();
sortedViewColumns.add(vc);
sortedModelColumns.add(new Integer(getModelColumn(e, viewColumn)));
sortingDirections.add(new Integer(SortableTableData.SORT_ASCENDING));
}
}
header.putClientProperty(SORTED_COLUMN_KEY, vectorToIntArray(sortedViewColumns));
header.putClientProperty(SORTING_DIRECTION_KEY, vectorToIntArray(sortingDirections));
model.sortColumns(
vectorToIntArray(sortedModelColumns),
vectorToIntArray(sortingDirections),
header.getTable());
header.repaint();
}
private int[] vectorToIntArray(Vector v) {
int[] ret = new int[v.size()];
for(int i = 0; i < ret.length; i++) {
ret[i] = ((Integer)v.get(i)).intValue();
}
return ret;
}
public void mousePressed(MouseEvent e) {
if(e.isPopupTrigger()) {
// showHeaderPopup(e);
return;
}
JTableHeader header = (JTableHeader)e.getSource();
pressedPoint = e.getPoint();
pressedColumn = header.columnAtPoint(pressedPoint);
mouseDragged = false;
}
public void mouseClicked(MouseEvent e) {}
// MouseMotionListener implementation
public void mouseDragged(MouseEvent e) {
SortableTableData model = getTableModel(e.getSource());
if(model == null) return;
inDrag = true;
JTableHeader header = (JTableHeader)e.getSource();
if(header.getResizingColumn() != null) {
// It's a resize, not a column move
if(!mouseDragged) mouseDragged = true;
}
if(!header.getReorderingAllowed()) {
int modelColumn = getModelColumnAt(e);
if(!model.isColumnSortable(modelColumn)) {
header.putClientProperty(ROLLOVER_COLUMN_KEY, null);
header.repaint();
return;
}
}
if(!mouseDragged && isMouseDragged(e.getPoint(), pressedPoint)) {
mouseDragged = true;
}
if(!mouseInside) {
// ! don't set rolloverColumn to -1
header.putClientProperty(ROLLOVER_COLUMN_KEY, null);
}
else {
if(!header.getReorderingAllowed()) {
int viewColumn = header.columnAtPoint(e.getPoint());
if(viewColumn != rolloverColumn) {
rolloverColumn = viewColumn;
}
}
if(rolloverColumn != -1) {
header.putClientProperty(ROLLOVER_COLUMN_KEY, new Integer(rolloverColumn));
}
}
header.repaint();
}
public void mouseMoved(MouseEvent e) {
if(!mouseInside) return;
JTableHeader header = (JTableHeader)e.getSource();
int viewColumn = header.columnAtPoint(e.getPoint());
if(viewColumn == -1) return;
SortableTableData model = getTableModel(e.getSource());
if(model == null) return;
int modelColumn = getModelColumnAt(e);
if(!model.isColumnSortable(modelColumn)) {
if(rolloverColumn != -1) {
rolloverColumn = -1;
header.putClientProperty(ROLLOVER_COLUMN_KEY, null);
header.repaint();
}
return;
}
// rolloverColumn must be viewColumn, not modelColumn
if(viewColumn != rolloverColumn) {
rolloverColumn = viewColumn;
header.putClientProperty(ROLLOVER_COLUMN_KEY, new Integer(rolloverColumn));
header.repaint();
}
}
// TableColumnModelListener implementation
public void columnAdded(TableColumnModelEvent e) {
// System.out.println("columnAdded at " + e.getToIndex());
removeSorting();
}
public void columnMoved(TableColumnModelEvent e) {
if(e.getFromIndex() == e.getToIndex()) return;
if(header == null) return;
// update rolloverColumn
if(rolloverColumn == e.getFromIndex()) {
rolloverColumn = e.getToIndex();
if(mouseInside) {
header.putClientProperty(ROLLOVER_COLUMN_KEY, new Integer(rolloverColumn));
}
}
// update sorted column(s)
int[] sc = vectorToIntArray(sortedViewColumns);
boolean changed = false;
// Note: With multiple sorted columns it might easily
// be that both the column at FromIndex and the column
// at ToIndex are sortable
for(int i = 0; i < sc.length; i++) {
if(sc[i] == e.getFromIndex()) {
sc[i] = e.getToIndex();
changed = true;
}
else if(sc[i] == e.getToIndex()) {
sc[i] = e.getFromIndex();
changed = true;
}
}
if(changed) {
sortedViewColumns.clear();
for(int i = 0; i < sc.length; i++) {
sortedViewColumns.add(new Integer(sc[i]));
}
header.putClientProperty(SORTED_COLUMN_KEY, vectorToIntArray(sortedViewColumns));
}
}
public void columnRemoved(TableColumnModelEvent e) {
// System.out.println("columnRemoved at " + e.getFromIndex());
removeSorting();
}
public void columnMarginChanged(ChangeEvent e) {}
public void columnSelectionChanged(ListSelectionEvent e) {}
// Helper methods
private void removeSorting() {
if(header == null) return;
// remove rolloverColumn
if(rolloverColumn != -1) {
rolloverColumn = -1;
header.putClientProperty(ROLLOVER_COLUMN_KEY, new Integer(rolloverColumn));
}
sortedModelColumns.clear();
sortedViewColumns.clear();
sortingDirections.clear();
header.putClientProperty(SORTING_DIRECTION_KEY, null);
header.putClientProperty(SORTED_COLUMN_KEY, null);
header.repaint();
}
void removeSortingInformation() {
if(header == null) return;
SortableTableData model = getTableModel(header);
if(model == null) return;
// cache sorting information
sortingCache.put(header,
new SortingInformation(
sortedViewColumns,
sortedModelColumns,
sortingDirections));
// We don't have to call removeSorting()
model.sortColumns(
new int[] {},
new int[] {},
header.getTable());
header.repaint();
}
void restoreSortingInformation(JTableHeader header, SortingInformation sortingInfo) {
if(header == null) return;
SortableTableData model = getTableModel(header);
if(model == null) return;
sortedViewColumns = sortingInfo.sortedViewColumns;
sortedModelColumns = sortingInfo.sortedModelColumns;
sortingDirections = sortingInfo.sortingDirections;
model.sortColumns(
vectorToIntArray(sortedModelColumns),
vectorToIntArray(sortingDirections),
header.getTable());
header.repaint();
}
/**
*
* @param source the table header
* @return the TableModel to work on or <code>null</code> if
* either table is <code>null</code>, table model is <code>null</code>
* or table model doesn't implement <code>SortableTableData</code>
*/
private SortableTableData getTableModel(Object source) {
return getTableModel((JTableHeader)source);
}
private SortableTableData getTableModel(JTableHeader header) {
JTable table = header.getTable();
if(table == null) return null;
TableModel model = table.getModel();
if(!(model instanceof SortableTableData)) return null;
// if(!tableModelListenerInstalled) {
// tableModelListenerInstalled = true;
// model.addTableModelListener(handler);
// }
return (SortableTableData)model;
}
/**
*
* @param e
* @return the logical column index
*/
private int getModelColumnAt(MouseEvent e) {
JTableHeader header = (JTableHeader)e.getSource();
int viewColumn = header.columnAtPoint(e.getPoint());
if(viewColumn == -1) return -1;
return header.getColumnModel().getColumn(viewColumn).getModelIndex();
}
/**
*
* @param e
* @param viewColumn
* @return the model column index corresponding to the
* view column index
*/
private int getModelColumn(MouseEvent e, int viewColumn) {
if(viewColumn == -1) return -1;
JTableHeader header = (JTableHeader)e.getSource();
return getModelColumn(header, viewColumn);
}
private int getModelColumn(JTableHeader header, int viewColumn) {
return header.getColumnModel().getColumn(viewColumn).getModelIndex();
}
private boolean isMouseDragged(Point p1, Point p2) {
if(Math.abs(p1.x - p2.x) >= MINIMUM_DRAG_DISTANCE) return true;
return false;
}
private void showHeaderPopup(MouseEvent e) {
final SortableTableData model = getTableModel(e.getSource());
if(model == null) return;
JTableHeader header = (JTableHeader)e.getSource();
final int viewColumn = header.columnAtPoint(e.getPoint());
JPopupMenu menu = new JPopupMenu();
JMenu sub = new JMenu("Add");
JMenuItem item = new JMenuItem("Double Column");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
((TinyTableModel)model).addColumn(Double.class, viewColumn);
}
});
sub.add(item);
item = new JMenuItem("Icon Column");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
((TinyTableModel)model).addColumn(Icon.class, viewColumn);
}
});
sub.add(item);
item = new JMenuItem("Integer Column");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
((TinyTableModel)model).addColumn(Integer.class, viewColumn);
}
});
sub.add(item);
item = new JMenuItem("String Column");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
((TinyTableModel)model).addColumn(String.class, viewColumn);
}
});
sub.add(item);
menu.add(sub);
menu.addSeparator();
item = new JMenuItem("Remove Column");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
((TinyTableModel)model).removeColumn(viewColumn);
}
});
if(((TinyTableModel)model).getColumnCount() < 2) {
item.setEnabled(false);
}
menu.add(item);
menu.addSeparator();
item = new JMenuItem("Remove all Rows");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
((TinyTableModel)model).removeAllRows();
}
});
if(((TinyTableModel)model).getRowCount() == 0) {
item.setEnabled(false);
}
menu.add(item);
menu.addSeparator();
item = new JMenuItem("Create new Data");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
((TinyTableModel)model).createNewData();
}
});
if(((TinyTableModel)model).getRowCount() > 0) {
item.setEnabled(false);
}
menu.add(item);
menu.show(header, e.getX(), 0);
}
}
}