0001 /*
0002 * Copyright (c) 1995-2010, The University of Sheffield. See the file
0003 * COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
0004 *
0005 * This file is part of GATE (see http://gate.ac.uk/), and is free
0006 * software, licenced under the GNU Library General Public License,
0007 * Version 2, June 1991 (in the distribution as file licence.html,
0008 * and also available at http://gate.ac.uk/gate/licence.html).
0009 *
0010 * XJTable.java
0011 *
0012 * Valentin Tablan, 25-Jun-2004
0013 *
0014 * $Id: XJTable.java 13462 2011-02-18 08:55:17Z markagreenwood $
0015 */
0016
0017 package gate.swing;
0018
0019 import java.awt.Component;
0020 import java.awt.Container;
0021 import java.awt.Dimension;
0022 import java.awt.Point;
0023 import java.awt.Rectangle;
0024 import java.awt.event.*;
0025 import java.util.*;
0026 import javax.swing.*;
0027 import javax.swing.event.ChangeEvent;
0028 import javax.swing.event.TableColumnModelEvent;
0029 import javax.swing.event.TableModelEvent;
0030 import javax.swing.event.TableModelListener;
0031 import javax.swing.table.*;
0032
0033 import gate.util.ObjectComparator;
0034
0035 /**
0036 * A "smarter" JTable. Features include:
0037 * <ul>
0038 * <li>sorting the table using the values from a column as keys</li>
0039 * <li>updating the widths of the columns so they accommodate the contents to
0040 * their preferred sizes.</li>
0041 * <li>filling the whole viewport by default, unless told not to auto resize
0042 * (see {@link JTable#setAutoResizeMode(int)}.
0043 * <li>sizing the rows according to the preferred sizes of the renderers</li>
0044 * <li>ability to hide/show columns</li>
0045 * <li>ability for tab key to skip uneditable cells
0046 * <li>ability to get in editing mode as soon as a cell gets the focus
0047 * </ul>
0048 * It uses a custom made model that stands between the table model set by the
0049 * user and the GUI component. This middle model is responsible for sorting the
0050 * rows.
0051 */
0052 public class XJTable extends JTable{
0053
0054 public XJTable(){
0055 this(null);
0056 }
0057
0058 public XJTable(TableModel model){
0059 super();
0060 if(model != null) setModel(model);
0061 // this is a partial fix for
0062 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4330950:
0063 // causes the current edit to be committed when focus moves to another
0064 // component. The other half of this bug (edits lost when the table
0065 // header is clicked) is being addressed by overriding
0066 // columnMarginChanged(ChangeEvent) and columnMoved(TableColumnModelEvent)
0067 putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
0068 }
0069
0070 public void setModel(TableModel dataModel) {
0071 sortingModel = new SortingModel(dataModel);
0072 componentSizedProperly = false;
0073 super.setModel(sortingModel);
0074 newColumns();
0075 }
0076
0077 /**
0078 * Called when the columns have changed.
0079 */
0080 protected void newColumns(){
0081 columnData = new ArrayList<ColumnData>(dataModel.getColumnCount());
0082 hiddenColumns = new ArrayList<TableColumn>();
0083 for(int i = 0; i < dataModel.getColumnCount(); i++) {
0084 columnData.add(new ColumnData(i));
0085 }
0086 }
0087
0088
0089 public void setTableHeader(JTableHeader newTableHeader) {
0090 //first remove the old listener from the old header
0091 JTableHeader oldHeader = getTableHeader();
0092 if(oldHeader != null && headerMouseListener != null)
0093 oldHeader.removeMouseListener(headerMouseListener);
0094 // set the new header
0095 super.setTableHeader(newTableHeader);
0096 //add the mouse listener to the new header
0097 if(headerMouseListener == null) headerMouseListener =
0098 new HeaderMouseListener();
0099 if(newTableHeader != null)
0100 newTableHeader.addMouseListener(headerMouseListener);
0101 }
0102
0103 public Dimension getPreferredScrollableViewportSize(){
0104 return getPreferredSize();
0105 }
0106
0107 protected void calculatePreferredSize(){
0108 try{
0109 if(sizingInProgress){
0110 return;
0111 }else{
0112 sizingInProgress = true;
0113 }
0114
0115 int colCount = getColumnModel().getColumnCount();
0116 if (colCount == 0) { return; }
0117 //recalculate the preferred sizes if anything changed
0118 if(!componentSizedProperly){
0119 Dimension spacing = getIntercellSpacing();
0120 //start with all columns at header size
0121 for(int col = 0; col < colCount; col++){
0122 TableColumn tColumn = getColumnModel().getColumn(col);
0123 TableCellRenderer headerRenderer = tColumn.getHeaderRenderer();
0124 boolean needToResetRenderer = false;
0125 if(headerRenderer == null){
0126 needToResetRenderer = true;
0127 //no header renderer provided -> use default implementation
0128 JTableHeader tHeader = getTableHeader();
0129 if(tHeader == null){
0130 tHeader = new JTableHeader();
0131 }
0132 headerRenderer = tHeader.getDefaultRenderer();
0133 tColumn.setHeaderRenderer(headerRenderer);
0134 }
0135 //initialise sizes for columns:
0136 // - min size = header size
0137 // - pref size = header size
0138 if(headerRenderer != null){
0139 Component c = headerRenderer.getTableCellRendererComponent(
0140 XJTable.this, tColumn.getHeaderValue(), false, false, 0, 0);
0141 int width = c.getMinimumSize().width + spacing.width;
0142 if(tColumn.getMinWidth() != width) tColumn.setMinWidth(width);
0143 tColumn.setPreferredWidth(width);
0144 }else{
0145 tColumn.setMinWidth(1);
0146 tColumn.setPreferredWidth(1);
0147 }
0148
0149 if (needToResetRenderer) tColumn.setHeaderRenderer(null);
0150 }
0151
0152 //now fix the row height and column min/max widths
0153 for(int row = 0; row < getRowCount(); row++){
0154 //start with all rows of size 1
0155 int newRowHeight = 1;
0156 // update the preferred size of the column ( to make it larger if any
0157 // components are larger than then header.
0158 for(int column = 0; column < getColumnCount(); column ++){
0159 Component cellComponent = prepareRenderer(getCellRenderer(row, column),
0160 row, column);
0161 TableColumn tColumn = getColumnModel().getColumn(column);
0162 int minWidth = cellComponent.getMinimumSize().width + spacing.width;
0163 //minimum width can only grow
0164 //if needed, increase the max width
0165 // if(tColumn.getMaxWidth() < minWidth) tColumn.setMaxWidth(minWidth);
0166 //we prefer not to have any extra space.
0167 if(tColumn.getPreferredWidth() < minWidth) tColumn.setPreferredWidth(minWidth);
0168
0169 //now fix the row height
0170 int height = cellComponent.getPreferredSize().height;
0171 if(newRowHeight < (height + spacing.height))
0172 newRowHeight = height + spacing.height;
0173 }
0174 setRowHeight(row, newRowHeight);
0175 }
0176 }//if(!componentSizedProperly){
0177
0178 //now adjust the column widths, if we need to fill all the space
0179 if(getAutoResizeMode() != AUTO_RESIZE_OFF){
0180 //the column being resized (if any)
0181 TableColumn resizingCol = null;
0182 if(getTableHeader() != null) resizingCol =
0183 getTableHeader().getResizingColumn();
0184
0185 int prefWidth = 0;
0186 for(int i = 0; i < colCount; i++){
0187 int colWidth = getColumnModel().getColumn(i).getPreferredWidth();
0188 if(colWidth > 0) prefWidth += colWidth;
0189 }
0190
0191 int requestedWidth = getWidth();
0192 Container parent = this.getParent();
0193 if(parent != null && parent instanceof JViewport) {
0194 // only track the viewport width if it is big enough.
0195 int viewportWidth = ((JViewport)parent).getExtentSize().width;
0196 if(viewportWidth > requestedWidth){
0197 requestedWidth = viewportWidth;
0198 }
0199 }
0200 int extra = 0;
0201
0202 if(requestedWidth > prefWidth){
0203 //we have extra space to fill
0204 extra = requestedWidth - prefWidth;
0205 if(getAutoResizeMode() == AUTO_RESIZE_ALL_COLUMNS){
0206 int extraCol = extra / colCount;
0207 //distribute the extra space to all columns
0208 for(int col = 0; col < colCount - 1; col++){
0209 TableColumn tColumn = getColumnModel().getColumn(col);
0210 //we leave the resizing column alone
0211 if(tColumn != resizingCol){
0212 tColumn.setPreferredWidth(tColumn.getPreferredWidth() + extraCol);
0213 extra -= extraCol;
0214 }
0215 }
0216 }
0217 //all the remainder goes to the last col
0218 TableColumn tColumn = getColumnModel().getColumn(colCount - 1);
0219 tColumn.setPreferredWidth(tColumn.getPreferredWidth() + extra);
0220 }//if(requestedWidth > prefWidth){
0221 }//if(getAutoResizeMode() != AUTO_RESIZE_OFF){
0222 }finally{
0223 componentSizedProperly = true;
0224 sizingInProgress = false;
0225 }
0226 }
0227
0228
0229
0230 @Override
0231 public void doLayout() {
0232 calculatePreferredSize();
0233 super.doLayout();
0234 }
0235
0236 @Override
0237 /**
0238 * Overridden so that the preferred size can be calculated properly
0239 */
0240 public Dimension getPreferredSize() {
0241 //if hard-coded, we return the given value.
0242 if(isPreferredSizeSet()) return super.getPreferredSize();
0243
0244 //the first time the component is sized, calculate the actual preferred size
0245 if(!componentSizedProperly){
0246 calculatePreferredSize();
0247 }
0248 return super.getPreferredSize();
0249 }
0250
0251 /**
0252 * Overridden to ignore requests for this table to track the width of its
0253 * containing viewport in cases where the viewport is narrower than the
0254 * minimum size of the table. Where the viewport is at least as wide as
0255 * the minimum size of the table, we will allow the table to resize with
0256 * the viewport, but when it gets too small we stop tracking, which allows
0257 * the horizontal scrollbar to appear.
0258 */
0259 @Override
0260 public boolean getScrollableTracksViewportWidth() {
0261 if(super.getScrollableTracksViewportWidth()) {
0262 Container parent = this.getParent();
0263 if(parent != null && parent instanceof JViewport) {
0264 // only track the viewport width if it is big enough.
0265 return ((JViewport)parent).getExtentSize().width
0266 > this.getPreferredSize().width;
0267 } else {
0268 return true;
0269 }
0270 }
0271 else { // super.getScrollableTracksViewportWidth() == false
0272 return false;
0273 }
0274 }
0275
0276 /**
0277 * Track parent viewport's size if it's larger than the current preferred
0278 * height of the table (causes empty tables to fill in the whole area of
0279 * a JScrollPane).
0280 * @return true if the preferred height of the table is smaller than the
0281 * viewport.
0282 */
0283 public boolean getScrollableTracksViewportHeight() {
0284 Container parent = getParent();
0285 Dimension dim = getPreferredSize();
0286 return (parent != null && dim!= null &&
0287 parent instanceof JViewport &&
0288 parent.getHeight() > getPreferredSize().height);
0289 }
0290
0291 private volatile boolean componentSizedProperly = false;
0292
0293 private volatile boolean sizingInProgress = false;
0294
0295
0296 /**
0297 * Converts a row number from the model co-ordinates system to the view's.
0298 * @param modelRow the row number in the model
0299 * @return the corresponding row number in the view.
0300 * @see #rowViewToModel(int)
0301 */
0302 public int rowModelToView(int modelRow){
0303 return sortingModel.sourceToTarget(modelRow);
0304 }
0305
0306 /**
0307 * @return Returns the ascending.
0308 */
0309 public boolean isAscending() {
0310 return ascending;
0311 }
0312
0313 /**
0314 * Gets the hidden state for a column
0315 * @param columnIndex index of the column in the table model
0316 * @return hidden state for a column
0317 */
0318 public boolean isColumnHidden(int columnIndex){
0319 for (TableColumn hiddenColumn : hiddenColumns) {
0320 if (hiddenColumn.getModelIndex() == columnIndex) {
0321 return true;
0322 }
0323 }
0324 return false;
0325 }
0326
0327 /**
0328 * Hide a column. Do nothing if already hidden.
0329 * @param columnIndex index of the column in the table model
0330 */
0331 public void hideColumn(int columnIndex) {
0332 int viewColumn = convertColumnIndexToView(columnIndex);
0333 TableColumn columnToHide = columnModel.getColumn(viewColumn);
0334 columnModel.removeColumn(columnToHide);
0335 hiddenColumns.add(columnToHide);
0336 }
0337
0338 /**
0339 * Show a column. Do nothing if already shown.
0340 * @param columnIndex index of the column in the table model
0341 * @param insertionIndex index of the view where the colum will be inserted
0342 */
0343 public void showColumn(int columnIndex, int insertionIndex) {
0344 for (TableColumn hiddenColumn : hiddenColumns) {
0345 if (hiddenColumn.getModelIndex() == columnIndex) {
0346 columnModel.addColumn(hiddenColumn);
0347 columnModel.moveColumn(columnModel.getColumnCount()-1, insertionIndex);
0348 hiddenColumns.remove(hiddenColumn);
0349 break;
0350 }
0351 }
0352 }
0353
0354 /**
0355 * @param ascending The ascending to set. True by default.
0356 */
0357 public void setAscending(boolean ascending) {
0358 this.ascending = ascending;
0359 }
0360 /**
0361 * Converts a row number from the view co-ordinates system to the model's.
0362 * @param viewRow the row number in the view.
0363 * @return the corresponding row number in the model.
0364 * @see #rowModelToView(int)
0365 */
0366 public int rowViewToModel(int viewRow){
0367 return sortingModel.targetToSource(viewRow);
0368 }
0369
0370 /**
0371 * Set the possibility for the user to hide/show a column by right-clicking
0372 * on a column header. False by default.
0373 * @param enableHidingColumns true if and only if the columns can be hidden.
0374 */
0375 public void setEnableHidingColumns(boolean enableHidingColumns) {
0376 this.enableHidingColumns = enableHidingColumns;
0377 }
0378
0379 /**
0380 * Returns the state for hiding a column.
0381 * @return true if and only if the columns can be hidden.
0382 */
0383 public boolean isEnableHidingColumns() {
0384 return this.enableHidingColumns;
0385 }
0386
0387 /**
0388 * Returns the state for editing a cell as soon as it gets the focus.
0389 * @return true if and only if a cell get in editing mode when
0390 * it receives the focus.
0391 */
0392 public boolean isEditCellAsSoonAsFocus() {
0393 return editCellAsSoonAsFocus;
0394 }
0395
0396 /**
0397 * Set the possibility for a cell to be in editing mode as soon as
0398 * it gets the focus. False by default.
0399 * @param editCellAsSoonAsFocus true if and only if a cell get in editing
0400 * mode when it receives the focus.
0401 */
0402 public void setEditCellAsSoonAsFocus(boolean editCellAsSoonAsFocus) {
0403 this.editCellAsSoonAsFocus = editCellAsSoonAsFocus;
0404 }
0405
0406 /**
0407 * Returns the state for enabling tab key to skip uneditable cells.
0408 * @return true if and only if the tab key skip uneditable cells.
0409 */
0410 public boolean isTabSkipUneditableCell() {
0411 return tabSkipUneditableCell;
0412 }
0413
0414 /**
0415 * Set the possibility for the tab key to skip uneditable cells.
0416 * False by default.
0417 * @param tabSkipUneditableCell true if and only if the tab key skip
0418 * uneditable cells.
0419 */
0420 public void setTabSkipUneditableCell(boolean tabSkipUneditableCell) {
0421
0422 this.tabSkipUneditableCell = tabSkipUneditableCell;
0423
0424 InputMap im = getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
0425 KeyStroke tab = KeyStroke.getKeyStroke("TAB");
0426 KeyStroke shiftTab = KeyStroke.getKeyStroke("shift TAB");
0427 if (oldTabAction == null) {
0428 oldTabAction = getActionMap().get(im.get(tab));
0429 oldShiftTabAction = getActionMap().get(im.get(shiftTab));
0430 }
0431
0432 if (tabSkipUneditableCell) {
0433
0434 // skip non editable cells when tabbing
0435 Action tabAction = new AbstractAction() {
0436 public void actionPerformed(ActionEvent e) {
0437 oldTabAction.actionPerformed(e);
0438 JTable table = (JTable) e.getSource();
0439 int row = table.getSelectedRow();
0440 int originalRow = row;
0441 int column = table.getSelectedColumn();
0442 int originalColumn = column;
0443 while(!table.isCellEditable(row, column)) {
0444 oldTabAction.actionPerformed(e);
0445 row = table.getSelectedRow();
0446 column = table.getSelectedColumn();
0447 // back to where we started, get out
0448 if(row == originalRow
0449 && column == originalColumn) {
0450 break;
0451 }
0452 }
0453 }
0454 };
0455 getActionMap().put(im.get(tab), tabAction);
0456
0457 // skip non editable cells when shift tabbing
0458 Action shiftTabAction = new AbstractAction() {
0459 public void actionPerformed(ActionEvent e) {
0460 oldShiftTabAction.actionPerformed(e);
0461 JTable table = (JTable) e.getSource();
0462 int row = table.getSelectedRow();
0463 int originalRow = row;
0464 int column = table.getSelectedColumn();
0465 int originalColumn = column;
0466 while(!table.isCellEditable(row, column)) {
0467 oldShiftTabAction.actionPerformed(e);
0468 row = table.getSelectedRow();
0469 column = table.getSelectedColumn();
0470 // back to where we started, get out
0471 if(row == originalRow
0472 && column == originalColumn) {
0473 break;
0474 }
0475 }
0476 }
0477 };
0478 getActionMap().put(im.get(shiftTab), shiftTabAction);
0479
0480 } else {
0481 getActionMap().put(im.get(tab), oldTabAction);
0482 getActionMap().put(im.get(shiftTab), oldShiftTabAction);
0483 }
0484 }
0485
0486
0487 /**
0488 * Sets the custom comparator to be used for a particular column. Columns that
0489 * don't have a custom comparator will be sorted using the natural order.
0490 * @param column the column index.
0491 * @param comparator the comparator to be used.
0492 */
0493 public void setComparator(int column, Comparator comparator){
0494 columnData.get(column).comparator = comparator;
0495 }
0496
0497 /**
0498 * @return Returns the sortable.
0499 */
0500 public boolean isSortable(){
0501 return sortable;
0502 }
0503 /**
0504 * @param sortable The sortable to set. True by default.
0505 */
0506 public void setSortable(boolean sortable){
0507 this.sortable = sortable;
0508 }
0509 /**
0510 * @return Returns the sortColumn.
0511 */
0512 public int getSortedColumn(){
0513 return sortedColumn;
0514 }
0515 /**
0516 * @param sortColumn The sortColumn to set. None (-1) by default.
0517 */
0518 public void setSortedColumn(int sortColumn){
0519 this.sortedColumn = sortColumn;
0520 }
0521
0522 /**
0523 * Get the row in the table for a row in the model.
0524 * @param modelRow row in the model
0525 * @return row in the table view
0526 */
0527 public int getTableRow(int modelRow){
0528 return sortingModel.sourceToTarget(modelRow);
0529 }
0530
0531 /**
0532 * Handles translations between an indexed data source and a permutation of
0533 * itself (like the translations between the rows in sorted table and the
0534 * rows in the actual unsorted model).
0535 */
0536 protected class SortingModel extends AbstractTableModel
0537 implements TableModelListener{
0538
0539 public SortingModel(TableModel sourceModel){
0540 compWrapper = new ValueHolderComparator();
0541 init(sourceModel);
0542 }
0543
0544 protected class ValueHolder{
0545 private Object value;
0546 private int index;
0547 public ValueHolder(Object value, int index) {
0548 super();
0549 this.value = value;
0550 this.index = index;
0551 }
0552 }
0553
0554 protected class ValueHolderComparator implements Comparator<ValueHolder>{
0555 private Comparator comparator;
0556
0557 protected Comparator getComparator() {
0558 return comparator;
0559 }
0560
0561 protected void setComparator(Comparator comparator) {
0562 this.comparator = comparator;
0563 }
0564
0565 public int compare(ValueHolder o1, ValueHolder o2) {
0566 return ascending ? comparator.compare(o1.value, o2.value) :
0567 comparator.compare(o1.value, o2.value) * -1;
0568 }
0569 }
0570
0571 protected void init(TableModel sourceModel){
0572 if(this.sourceModel != null)
0573 this.sourceModel.removeTableModelListener(this);
0574 this.sourceModel = sourceModel;
0575 //start with the identity order
0576 int size = sourceModel.getRowCount();
0577 sourceToTarget = new int[size];
0578 targetToSource = new int[size];
0579 for(int i = 0; i < size; i++) {
0580 sourceToTarget[i] = i;
0581 targetToSource[i] = i;
0582 }
0583 sourceModel.addTableModelListener(this);
0584 if(isSortable() && sortedColumn == -1) setSortedColumn(0);
0585 componentSizedProperly = false;
0586 }
0587
0588 /**
0589 * This gets events from the source model and forwards them to the UI
0590 */
0591 public void tableChanged(TableModelEvent e){
0592 int type = e.getType();
0593 int firstRow = e.getFirstRow();
0594 int lastRow = e.getLastRow();
0595 int column = e.getColumn();
0596
0597 //deal with the changes in the data
0598 //we have no way to "repair" the sorting on data updates so we will need
0599 //to rebuild the order every time
0600
0601 switch(type){
0602 case TableModelEvent.UPDATE:
0603 if(firstRow == TableModelEvent.HEADER_ROW){
0604 //complete structure change -> reallocate the data
0605 init(sourceModel);
0606 newColumns();
0607 fireTableStructureChanged();
0608 if(isSortable()) sort();
0609 }else if(lastRow == Integer.MAX_VALUE){
0610 //all data changed (including the number of rows)
0611 init(sourceModel);
0612 if(isSortable()){
0613 sort();
0614 } else {
0615 //this will re-create all rows (which deletes the rowModel in JTable)
0616 componentSizedProperly = false;
0617 fireTableDataChanged();
0618 }
0619 }else{
0620 //the rows should have normal values
0621 //if the sortedColumn is not affected we don't care
0622 if(isSortable() &&
0623 (column == sortedColumn ||
0624 column == TableModelEvent.ALL_COLUMNS)){
0625 //re-sorting will also fire the event upwards
0626 sort();
0627 }else{
0628 componentSizedProperly = false;
0629 fireTableChanged(new TableModelEvent(this,
0630 sourceToTarget(firstRow),
0631 sourceToTarget(lastRow), column, type));
0632
0633 }
0634 }
0635 break;
0636 case TableModelEvent.INSERT:
0637 //rows were inserted -> we need to rebuild
0638 init(sourceModel);
0639 if(firstRow != TableModelEvent.HEADER_ROW && firstRow == lastRow){
0640 //single row insertion
0641 if(isSortable()) sort();
0642 else{
0643 componentSizedProperly = false;
0644 fireTableChanged(new TableModelEvent(this,
0645 sourceToTarget(firstRow),
0646 sourceToTarget(lastRow), column, type));
0647 }
0648 }else{
0649 //the real rows are not in sequence
0650 if(isSortable()) sort();
0651 else {
0652 //this will re-create all rows (which deletes the rowModel in JTable)
0653 componentSizedProperly = false;
0654 fireTableDataChanged();
0655 }
0656 }
0657 break;
0658 case TableModelEvent.DELETE:
0659 //rows were deleted -> we need to rebuild
0660 init(sourceModel);
0661 if(isSortable()) sort();
0662 else {
0663 //this will re-create all rows (which deletes the rowModel in JTable)
0664 componentSizedProperly = false;
0665 fireTableDataChanged();
0666 }
0667 }
0668 }
0669
0670 public int getRowCount(){
0671 return sourceToTarget.length;
0672 // return sourceModel.getRowCount();
0673 }
0674
0675 public int getColumnCount(){
0676 return sourceModel.getColumnCount();
0677 }
0678
0679 public String getColumnName(int columnIndex){
0680 return sourceModel.getColumnName(columnIndex);
0681 }
0682 public Class getColumnClass(int columnIndex){
0683 return sourceModel.getColumnClass(columnIndex);
0684 }
0685
0686 public boolean isCellEditable(int rowIndex, int columnIndex){
0687 return sourceModel.isCellEditable(targetToSource(rowIndex),
0688 columnIndex);
0689 }
0690 public void setValueAt(Object aValue, int rowIndex, int columnIndex){
0691 sourceModel.setValueAt(aValue, targetToSource(rowIndex),
0692 columnIndex);
0693 }
0694 public Object getValueAt(int row, int column){
0695 try{
0696 return sourceModel.getValueAt(targetToSource(row), column);
0697 }catch(IndexOutOfBoundsException iobe){
0698 //this can occur because of multithreaded access -> some threads empties
0699 //the data while some other thread tries to update the display.
0700 //this error is safe to ignore as the GUI will get updated by another
0701 //event once the change to the data has been effected
0702 return null;
0703 }
0704 }
0705
0706 /**
0707 * Sorts the table using the values in the specified column and sorting order.
0708 * sortedColumn is the column used for sorting the data.
0709 * ascending is the sorting order.
0710 */
0711 public void sort(){
0712 try {
0713 if (sortedColumn >= columnData.size()) { return; }
0714 //save the selection
0715 int[] rows = getSelectedRows();
0716 //convert to model co-ordinates
0717 for(int i = 0; i < rows.length; i++) rows[i] = rowViewToModel(rows[i]);
0718 //create a list of ValueHolder objects for the source data that needs
0719 //sorting
0720 List<ValueHolder> sourceData =
0721 new ArrayList<ValueHolder>(sourceModel.getRowCount());
0722 //get the data in the source order
0723 for(int i = 0; i < sourceModel.getRowCount(); i++){
0724 Object value = sourceModel.getValueAt(i, sortedColumn);
0725 sourceData.add(new ValueHolder(value, i));
0726 }
0727
0728 //get an appropriate comparator
0729 Comparator comparator = columnData.get(sortedColumn).comparator;
0730 if(comparator == null){
0731 //use the default comparator
0732 if(defaultComparator == null) defaultComparator = new ObjectComparator();
0733 comparator = defaultComparator;
0734 }
0735 compWrapper.setComparator(comparator);
0736 //perform the actual sorting
0737 Collections.sort(sourceData, compWrapper);
0738 //extract the new order
0739 for(int i = 0; i < sourceData.size(); i++){
0740 int targetIndex = i;
0741 int sourceIndex = sourceData.get(i).index;
0742 sourceToTarget[sourceIndex] = targetIndex;
0743 targetToSource[targetIndex] = sourceIndex;
0744 }
0745 sourceData.clear();
0746 //the rows may have moved about so we need to recalculate the heights
0747 componentSizedProperly = false;
0748 //this deletes the JTable row model
0749 fireTableDataChanged();
0750 //we need to recreate the row model, and only then restore selection
0751 resizeAndRepaint();
0752 //restore the selection
0753 clearSelection();
0754 for(int i = 0; i < rows.length; i++){
0755 rows[i] = rowModelToView(rows[i]);
0756 if(rows[i] > 0 && rows[i] < getRowCount())
0757 addRowSelectionInterval(rows[i], rows[i]);
0758 }
0759 }catch(ArrayIndexOutOfBoundsException aioob) {
0760 //this can happen when update events get behind
0761 //just ignore - we'll get another event later to cause the sorting
0762 }
0763 }
0764 /**
0765 * Converts an index from the source coordinates to the target ones.
0766 * Used to propagate events from the data source (table model) to the view.
0767 * @param index the index in the source coordinates.
0768 * @return the corresponding index in the target coordinates or -1 if the
0769 * provided row is outside the permitted values.
0770 */
0771 public int sourceToTarget(int index){
0772 return (index >= 0 && index < sourceToTarget.length) ?
0773 sourceToTarget[index] :
0774 -1;
0775 }
0776
0777 /**
0778 * Converts an index from the target coordinates to the source ones.
0779 * @param index the index in the target coordinates.
0780 * Used to propagate events from the view (e.g. editing) to the source
0781 * data source (table model).
0782 * @return the corresponding index in the source coordinates or -1 if the
0783 * row provided is outside the allowed range.
0784 */
0785 public int targetToSource(int index){
0786 return (index >= 0 && index < targetToSource.length) ?
0787 targetToSource[index] :
0788 -1;
0789 }
0790
0791 /**
0792 * Builds the reverse index based on the new sorting order.
0793 */
0794 protected void buildTargetToSourceIndex(){
0795 targetToSource = new int[sourceToTarget.length];
0796 for(int i = 0; i < sourceToTarget.length; i++)
0797 targetToSource[sourceToTarget[i]] = i;
0798 }
0799
0800 /**
0801 * The direct index
0802 */
0803 protected int[] sourceToTarget;
0804
0805 /**
0806 * The reverse index.
0807 */
0808 protected int[] targetToSource;
0809
0810 protected TableModel sourceModel;
0811
0812 protected ValueHolderComparator compWrapper;
0813 }
0814
0815 protected class HeaderMouseListener extends MouseAdapter{
0816 public void mouseClicked(MouseEvent e){
0817 process(e);
0818 }
0819
0820 public void mousePressed(MouseEvent e){
0821 process(e);
0822 }
0823
0824 public void mouseReleased(MouseEvent e){
0825 process(e);
0826 }
0827
0828 protected void process(MouseEvent e){
0829 final int viewColumn = columnModel.getColumnIndexAtX(e.getX());
0830 if(viewColumn != -1){
0831 final int column = convertColumnIndexToModel(viewColumn);
0832 if(e.isPopupTrigger()
0833 && enableHidingColumns){
0834 //show pop-up
0835 JPopupMenu popup = new JPopupMenu();
0836 if (columnModel.getColumnCount() > 1) {
0837 popup.add(new AbstractAction("Hide column "
0838 + dataModel.getColumnName(column)){
0839 public void actionPerformed(ActionEvent e) {
0840 hideColumn(column);
0841 }
0842 });
0843 }
0844 if (hiddenColumns.size() > 0) {
0845 popup.addSeparator();
0846 }
0847 for (TableColumn hiddenColumn : hiddenColumns) {
0848 final TableColumn hiddenColumnF = hiddenColumn;
0849 popup.add(new AbstractAction("Show column "
0850 + dataModel.getColumnName(hiddenColumn.getModelIndex())){
0851 public void actionPerformed(ActionEvent e) {
0852 showColumn(hiddenColumnF.getModelIndex(), viewColumn);
0853 }
0854 });
0855 }
0856 popup.show(e.getComponent(), e.getX(), e.getY());
0857 }
0858 else if(!e.isPopupTrigger()
0859 && e.getID() == MouseEvent.MOUSE_CLICKED
0860 && e.getClickCount() == 1){
0861 //normal click -> re-sort
0862 if(sortable && column != -1) {
0863 ascending = (column == sortedColumn) ? !ascending : true;
0864 sortedColumn = column;
0865 sortingModel.sort();
0866 }
0867 }
0868 }
0869 }
0870 }
0871
0872 protected class ColumnData{
0873 public ColumnData(int column){
0874 this.column = column;
0875 }
0876
0877 int column;
0878 int columnWidth;
0879 Comparator comparator;
0880 }
0881
0882 public void changeSelection(int rowIndex, int columnIndex,
0883 boolean toggle, boolean extend) {
0884 super.changeSelection(rowIndex, columnIndex, toggle, extend);
0885 if (!toggle && !extend && editCellAsSoonAsFocus) {
0886 // start cell editing as soon as a cell is selected
0887 if(!isEditing() && isCellEditable(rowIndex, columnIndex)) this.editCellAt(rowIndex, columnIndex);
0888 }
0889 }
0890
0891 @Override
0892 public int rowAtPoint(Point point) {
0893 //The Swing implementation is very keen to drop the rowModel. We KNOW we
0894 //should always have a row model, so if null, create it!
0895 if(!componentSizedProperly){
0896 calculatePreferredSize();
0897 }
0898 return super.rowAtPoint(point);
0899 }
0900
0901 /**
0902 * Overridden for efficiency reasons (provides a better calculation of the
0903 * dirty region). See
0904 * <a href="http://www.objectdefinitions.com/odblog/2009/jtable-setrowheight-causes-slow-repainting/">this page</a>
0905 * for a more complete discussion.
0906 */
0907 public void tableChanged(TableModelEvent e) {
0908 //if just an update, and not a data or structure changed event or an insert or delete, use the fixed row update handling
0909 //otherwise call super.tableChanged to let the standard JTable update handling manage it
0910 if ( e != null &&
0911 e.getType() == TableModelEvent.UPDATE &&
0912 e.getFirstRow() != TableModelEvent.HEADER_ROW &&
0913 e.getLastRow() != Integer.MAX_VALUE) {
0914 handleRowUpdate(e);
0915 } else {
0916 super.tableChanged(e);
0917 }
0918 }
0919
0920 /**
0921 * This borrows most of the logic from the superclass handling of update
0922 * events, but changes the calculation of the height for the dirty region to
0923 * provide proper handling for repainting custom height rows.
0924 * Copied from <a href="http://www.objectdefinitions.com/odblog/2009/jtable-setrowheight-causes-slow-repainting/">this page</a>.
0925 */
0926 private void handleRowUpdate(TableModelEvent e) {
0927 int modelColumn = e.getColumn();
0928 int start = e.getFirstRow();
0929 int end = e.getLastRow();
0930
0931 Rectangle dirtyRegion;
0932 if (modelColumn == TableModelEvent.ALL_COLUMNS) {
0933 // 1 or more rows changed
0934 int rowStart = 0;
0935 for ( int row=0; row < start; row++ ) rowStart += getRowHeight(row);
0936 dirtyRegion = new Rectangle(0, rowStart,
0937 getColumnModel().getTotalColumnWidth(), 0);
0938
0939 } else {
0940 // A cell or column of cells has changed.
0941 // Unlike the rest of the methods in the JTable, the TableModelEvent
0942 // uses the coordinate system of the model instead of the view.
0943 // This is the only place in the JTable where this "reverse mapping"
0944 // is used.
0945 int column = convertColumnIndexToView(modelColumn);
0946 dirtyRegion = getCellRect(start, column, true);
0947 }
0948
0949 // Now adjust the height of the dirty region
0950 dirtyRegion.height = 0;
0951 for (int row = start; row <= end; row ++ ) {
0952 //THIS IS CHANGED TO CALCULATE THE DIRTY REGION HEIGHT CORRECTLY
0953 dirtyRegion.height += getRowHeight(row);
0954 }
0955 repaint(dirtyRegion.x, dirtyRegion.y, dirtyRegion.width, dirtyRegion.height);
0956 }
0957
0958
0959 /**
0960 * Overridden to fix
0961 * //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4330950:
0962 */
0963 public void columnMoved(TableColumnModelEvent e) {
0964 if (isEditing()) {
0965 cellEditor.stopCellEditing();
0966 }
0967 super.columnMoved(e);
0968 }
0969
0970 /**
0971 * Overridden to fix
0972 * //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4330950:
0973 */
0974 public void columnMarginChanged(ChangeEvent e) {
0975 //save the current edit (otherwise it gets lost)
0976 if (isEditing()) cellEditor.stopCellEditing();
0977 //allow resizing of columns even if JTable believes we should block it.
0978 TableColumn resizingCol = null;
0979 if(getTableHeader() != null) resizingCol =
0980 getTableHeader().getResizingColumn();
0981 if (resizingCol != null) {
0982 resizingCol.setPreferredWidth(resizingCol.getWidth());
0983 }
0984
0985 super.columnMarginChanged(e);
0986 }
0987
0988 protected SortingModel sortingModel;
0989 protected ObjectComparator defaultComparator;
0990
0991 /**
0992 * The column currently being sorted.
0993 */
0994 protected int sortedColumn = -1;
0995
0996 /**
0997 * is the current sort order ascending (or descending)?
0998 */
0999 protected boolean ascending = true;
1000 /**
1001 * Should this table be sortable.
1002 */
1003 protected boolean sortable = true;
1004
1005 /**
1006 * A list of {@link ColumnData} objects.
1007 */
1008 protected List<ColumnData> columnData;
1009
1010 protected HeaderMouseListener headerMouseListener;
1011
1012 /**
1013 * Contains the hidden columns in no particular order.
1014 */
1015 protected List<TableColumn> hiddenColumns;
1016
1017 private boolean enableHidingColumns = false;
1018
1019 private boolean tabSkipUneditableCell = false;
1020
1021 private boolean editCellAsSoonAsFocus = false;
1022
1023 private Action oldTabAction;
1024
1025 private Action oldShiftTabAction;
1026 }
|