components/visual-panels/core/src/java/util/com/oracle/solaris/vp/util/swing/ExtTable.java
changeset 3553 f1d133b09a8c
parent 3552 077ebe3d0d24
child 3554 ef58713bafc4
equal deleted inserted replaced
3552:077ebe3d0d24 3553:f1d133b09a8c
     1 /*
       
     2  * CDDL HEADER START
       
     3  *
       
     4  * The contents of this file are subject to the terms of the
       
     5  * Common Development and Distribution License (the "License").
       
     6  * You may not use this file except in compliance with the License.
       
     7  *
       
     8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
       
     9  * or http://www.opensolaris.org/os/licensing.
       
    10  * See the License for the specific language governing permissions
       
    11  * and limitations under the License.
       
    12  *
       
    13  * When distributing Covered Code, include this CDDL HEADER in each
       
    14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
       
    15  * If applicable, add the following below this CDDL HEADER, with the
       
    16  * fields enclosed by brackets "[]" replaced with your own identifying
       
    17  * information: Portions Copyright [yyyy] [name of copyright owner]
       
    18  *
       
    19  * CDDL HEADER END
       
    20  */
       
    21 
       
    22 /*
       
    23  * Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
       
    24  */
       
    25 
       
    26 package com.oracle.solaris.vp.util.swing;
       
    27 
       
    28 import java.awt.*;
       
    29 import java.awt.event.*;
       
    30 import java.util.*;
       
    31 import java.util.List;
       
    32 import javax.swing.*;
       
    33 import javax.swing.border.Border;
       
    34 import javax.swing.event.*;
       
    35 import javax.swing.plaf.TableUI;
       
    36 import javax.swing.table.*;
       
    37 
       
    38 /**
       
    39  * The {@code ExtTable} class is a {@code JTable} with the following extras:
       
    40  *
       
    41  * <h4>Striping</h4>
       
    42  *
       
    43  * The {@code ExtTable} is decorated by default with alternating colored rows:
       
    44  * 1) the table's background, and 2) {@link #setStripeColor the stripe color}.
       
    45  * To turn this behavior off, call {@link #setStripeColor setStripeColor}{@code
       
    46  * (null)}.
       
    47  *
       
    48  * <h4>Caching</h4>
       
    49  *
       
    50  * The {@code ExtTable} can optionally cache the values it finds in its
       
    51  * {@code TableModel}.	This can speed performance when the model's {@code
       
    52  * getValueAt} method is time-consuming.  This feature is off by default, but
       
    53  * can be enabled by {@link #setCacheSize setting the size of the cache} to a
       
    54  * non-zero number of rows.
       
    55  *
       
    56  * <h4>Specifiable visible row count</h4>
       
    57  *
       
    58  * This feature, found in the {@code JList} API, {@link #setVisibleRowCount
       
    59  * sets} the preferred number of rows to display without requiring
       
    60  * scrolling.  This feature is off by default.
       
    61  *
       
    62  * <h4>Automatically-sized column widths</h4>
       
    63  *
       
    64  * Columns in an {@code ExtTable} can be easily sized to fit their content,
       
    65  * overriding the constant column widths set by {@code JTable}.  This can be
       
    66  * done {@link #fitColumns on demand} or {@link #setAutoFitColumns
       
    67  * automatically} whenever the table changes.  This feature is off by default.
       
    68  */
       
    69 @SuppressWarnings({"serial"})
       
    70 public class ExtTable extends JTable {
       
    71     //
       
    72     // Enums
       
    73     //
       
    74 
       
    75     public enum ColumnWidthPolicy {
       
    76 	/**
       
    77 	 * Column width is the maximum preferred width of each cell and
       
    78 	 * header in the column.
       
    79 	 */
       
    80 	PREFERRED,
       
    81 
       
    82 	/**
       
    83 	 * Columns widths are sized equally, based on the maximum value of all
       
    84 	 * columns' preferred widths.
       
    85 	 */
       
    86 	EQUAL
       
    87     }
       
    88 
       
    89     //
       
    90     // Inner classes
       
    91     //
       
    92 
       
    93     private static class CachedColumn<T> {
       
    94 	//
       
    95 	// Instance data
       
    96 	//
       
    97 
       
    98 	public int col;
       
    99 	public T value;
       
   100 
       
   101 	//
       
   102 	// Constructors
       
   103 	//
       
   104 
       
   105 	public CachedColumn(int col, T value) {
       
   106 	    this.col = col;
       
   107 	    this.value = value;
       
   108 	}
       
   109     }
       
   110 
       
   111     private static class CachedRow<T> {
       
   112 	//
       
   113 	// Instance data
       
   114 	//
       
   115 
       
   116 	public int row;
       
   117 	public List<CachedColumn<T>> cols = new LinkedList<CachedColumn<T>>();
       
   118 
       
   119 	//
       
   120 	// Constructors
       
   121 	//
       
   122 
       
   123 	public CachedRow(int row) {
       
   124 	    this.row = row;
       
   125 	}
       
   126     }
       
   127 
       
   128     //
       
   129     // Static data
       
   130     //
       
   131 
       
   132     public static final Color DEFAULT_STRIPE_COLOR =
       
   133 	new Color(242, 242, 242);
       
   134 //	new Color(237, 243, 254);
       
   135 
       
   136     //
       
   137     // Instance data
       
   138     //
       
   139 
       
   140     private int visibleRowCount = -1;
       
   141     private Color stripeColor = DEFAULT_STRIPE_COLOR;
       
   142 
       
   143     private List<CachedRow<Object>> valueCache =
       
   144 	new LinkedList<CachedRow<Object>>();
       
   145     private int valueCacheSize = 0;
       
   146 
       
   147     private Map<TableColumn, ArrayList<Integer>> widthCache =
       
   148 	new HashMap<TableColumn, ArrayList<Integer>>();
       
   149     private ColumnWidthPolicy colWidthPolicy;
       
   150 
       
   151     private TableModelListener modelListener;
       
   152     private TableColumnModelListener columnListener;
       
   153     private RowSorterListener sorterListener;
       
   154 
       
   155     //
       
   156     // Constructors
       
   157     //
       
   158 
       
   159     public ExtTable() {
       
   160 	init();
       
   161     }
       
   162 
       
   163     public ExtTable(int numRows, int numColumns) {
       
   164 	super(numRows, numColumns);
       
   165 	init();
       
   166     }
       
   167 
       
   168     public ExtTable(Object[][] rowData, Object[] columnNames) {
       
   169 	super(rowData, columnNames);
       
   170 	init();
       
   171     }
       
   172 
       
   173     public ExtTable(TableModel model) {
       
   174 	super(model);
       
   175 	init();
       
   176     }
       
   177 
       
   178     public ExtTable(TableModel model, TableColumnModel cModel) {
       
   179 	super(model, cModel);
       
   180 	init();
       
   181     }
       
   182 
       
   183     public ExtTable(TableModel model, TableColumnModel cModel,
       
   184 	ListSelectionModel sModel) {
       
   185 
       
   186 	super(model, cModel, sModel);
       
   187 	init();
       
   188     }
       
   189 
       
   190     public ExtTable(Vector rowData, Vector columnNames) {
       
   191 	super(rowData, columnNames);
       
   192 	init();
       
   193     }
       
   194 
       
   195     //
       
   196     // JComponent methods
       
   197     //
       
   198 
       
   199     @Override
       
   200     protected void paintComponent(Graphics g) {
       
   201 	Rectangle clip = g.getClipBounds();
       
   202 	int rHeight = getRowHeight();
       
   203 	int fRow = clip.y / rHeight;
       
   204 	int lRow = (clip.y + clip.height) / rHeight + 1;
       
   205 
       
   206 	if (isOpaque()) {
       
   207 	    g.setColor(getBackground());
       
   208 	    g.fillRect(clip.x, clip.y, clip.width, clip.height);
       
   209 	}
       
   210 
       
   211 	if (stripeColor != null) {
       
   212 	    g.setColor(stripeColor);
       
   213 	    if ((fRow & 1) == 1) {
       
   214 		fRow++;
       
   215 	    }
       
   216 
       
   217 	    for (int row = fRow; row < lRow; row += 2) {
       
   218 		g.fillRect(clip.x, rHeight * row, clip.width, rHeight);
       
   219 	    }
       
   220 	}
       
   221 
       
   222 	if (getShowHorizontalLines()) {
       
   223 	    g.setColor(getGridColor());
       
   224 	    for (int row = fRow; row < lRow; row++) {
       
   225 		int y = rHeight * row - 1;
       
   226 		g.drawLine(clip.x, y, clip.width - 1, y);
       
   227 	    }
       
   228 	}
       
   229 
       
   230 	if (getShowVerticalLines()) {
       
   231 	    g.setColor(getGridColor());
       
   232 
       
   233 	    TableColumnModel model = getColumnModel();
       
   234 	    int x = -1;
       
   235 	    for (int col = 0, n = model.getColumnCount(); col < n; col++) {
       
   236 		x += model.getColumn(col).getWidth();
       
   237 
       
   238 		if (x >= clip.x) {
       
   239 		    if (x > clip.x + clip.width) {
       
   240 			break;
       
   241 		    }
       
   242 		    g.drawLine(x, clip.y, x, clip.y + clip.height - 1);
       
   243 		}
       
   244 	    }
       
   245 	}
       
   246 
       
   247 	getUI().paint(g, this);
       
   248     }
       
   249 
       
   250     //
       
   251     // JTable methods
       
   252     //
       
   253 
       
   254     @Override
       
   255     public Dimension getPreferredScrollableViewportSize() {
       
   256 	if (visibleRowCount < 0) {
       
   257 	    return super.getPreferredScrollableViewportSize();
       
   258 	}
       
   259 
       
   260 	Insets insets = getInsets();
       
   261 	Dimension d = getPreferredSize();
       
   262 	d.height = visibleRowCount * getRowHeight() + insets.top +
       
   263 	    insets.bottom;
       
   264 
       
   265 	return d;
       
   266     }
       
   267 
       
   268     @Override
       
   269     public Object getValueAt(int vRow, int vCol) {
       
   270 	if (valueCacheSize > 0) {
       
   271 	    CachedRow<Object> row = null;
       
   272 	    ListIterator<CachedRow<Object>> rowIter = valueCache.listIterator();
       
   273 
       
   274 	    while (rowIter.hasNext()) {
       
   275 		CachedRow<Object> r = rowIter.next();
       
   276 
       
   277 		if (r.row == vRow) {
       
   278 		    row = r;
       
   279 		    break;
       
   280 		}
       
   281 
       
   282 		if (r.row > vRow) {
       
   283 		    rowIter.previous();
       
   284 		    break;
       
   285 		}
       
   286 	    }
       
   287 
       
   288 	    if (row == null) {
       
   289 		row = new CachedRow<Object>(vRow);
       
   290 
       
   291 		rowIter.add(row);
       
   292 
       
   293 		if (valueCache.size() > valueCacheSize) {
       
   294 		    // Trim cached row farthest from the new one
       
   295 		    int index = rowIter.previousIndex();
       
   296 		    int lastIndex = valueCache.size() - 1;
       
   297 		    valueCache.remove(lastIndex - index > index - 0 ?
       
   298 			lastIndex : 0);
       
   299 		}
       
   300 	    }
       
   301 
       
   302 	    CachedColumn<Object> col = null;
       
   303 	    ListIterator<CachedColumn<Object>> colIter =
       
   304 		row.cols.listIterator();
       
   305 
       
   306 	    while (colIter.hasNext()) {
       
   307 		CachedColumn<Object> c = colIter.next();
       
   308 
       
   309 		if (c.col == vCol) {
       
   310 		    col = c;
       
   311 		    break;
       
   312 		}
       
   313 
       
   314 		if (c.col > vCol) {
       
   315 		    colIter.previous();
       
   316 		    break;
       
   317 		}
       
   318 	    }
       
   319 
       
   320 	    if (col == null) {
       
   321 		Object value = super.getValueAt(vRow, vCol);
       
   322 		col = new CachedColumn<Object>(vCol, value);
       
   323 		colIter.add(col);
       
   324 	    }
       
   325 
       
   326 	    return col.value;
       
   327 	}
       
   328 
       
   329 	return super.getValueAt(vRow, vCol);
       
   330     }
       
   331 
       
   332     @Override
       
   333     public Component prepareRenderer(TableCellRenderer renderer, int row,
       
   334         int col) {
       
   335 
       
   336         Component comp = super.prepareRenderer(renderer, row, col);
       
   337         if (comp instanceof JComponent) {
       
   338             ((JComponent)comp).setOpaque(isCellSelected(row, col));
       
   339         }
       
   340 
       
   341         return comp;
       
   342     }
       
   343 
       
   344     @Override
       
   345     public void setColumnModel(TableColumnModel columnModel) {
       
   346 	TableColumnModel old = super.getColumnModel();
       
   347 	if (old != columnModel) {
       
   348 	    if (columnListener == null) {
       
   349 		// Create this here since object members haven't yet been
       
   350 		// initialized if this method is being called from a superclass
       
   351 		// constructor
       
   352 		columnListener = new TableColumnModelListener() {
       
   353 		    @Override
       
   354 		    public void columnAdded(TableColumnModelEvent e) {
       
   355 			fitColumnsIfRequested();
       
   356 		    }
       
   357 
       
   358 		    @Override
       
   359 		    public void columnMarginChanged(ChangeEvent e) {
       
   360 		    }
       
   361 
       
   362 		    @Override
       
   363 		    public void columnMoved(TableColumnModelEvent e) {
       
   364 		    }
       
   365 
       
   366 		    @Override
       
   367 		    public void columnRemoved(TableColumnModelEvent e) {
       
   368 			deleteUnusedColumnsFromWidthCache();
       
   369 		    }
       
   370 
       
   371 		    @Override
       
   372 		    public void columnSelectionChanged(ListSelectionEvent e) {
       
   373 		    }
       
   374 		};
       
   375 	    }
       
   376 
       
   377 	    if (old != null) {
       
   378 		old.removeColumnModelListener(columnListener);
       
   379 	    }
       
   380 
       
   381 	    if (columnModel != null) {
       
   382 		columnModel.addColumnModelListener(columnListener);
       
   383 	    }
       
   384 
       
   385 	    super.setColumnModel(columnModel);
       
   386 
       
   387 	    clearWidthCache();
       
   388 	    fitColumnsIfRequested();
       
   389 	}
       
   390     }
       
   391 
       
   392     @Override
       
   393     public void setTableHeader(JTableHeader header) {
       
   394 	JTableHeader old = super.getTableHeader();
       
   395 	if (old != header) {
       
   396 	    super.setTableHeader(header);
       
   397 
       
   398 	    clearWidthCache();
       
   399 	    fitColumnsIfRequested();
       
   400 	}
       
   401     }
       
   402 
       
   403     @Override
       
   404     public void setModel(TableModel model) {
       
   405 	TableModel old = super.getModel();
       
   406 	if (old != model) {
       
   407 	    if (modelListener == null) {
       
   408 		// Create this here since object members haven't yet been
       
   409 		// initialized if this method is being called from a superclass
       
   410 		// constructor
       
   411 		modelListener = new TableModelListener() {
       
   412 		    @Override
       
   413 		    public void tableChanged(TableModelEvent e) {
       
   414 			valueCache.clear();
       
   415 			updateWidthCache(e);
       
   416 		    }
       
   417 		};
       
   418 	    }
       
   419 
       
   420 	    if (old != null) {
       
   421 		old.removeTableModelListener(modelListener);
       
   422 	    }
       
   423 
       
   424 	    if (model != null) {
       
   425 		model.addTableModelListener(modelListener);
       
   426 	    }
       
   427 
       
   428 	    super.setModel(model);
       
   429 
       
   430 	    clearWidthCache();
       
   431 	    fitColumnsIfRequested();
       
   432 	}
       
   433     }
       
   434 
       
   435     @Override
       
   436     public void setRowSorter(RowSorter<? extends TableModel> sorter) {
       
   437 	RowSorter<? extends TableModel> oldSorter = getRowSorter();
       
   438 	if (sorter != oldSorter) {
       
   439 	    if (sorterListener == null) {
       
   440 		// Create this here since object members haven't yet been
       
   441 		// initialized if this method is being called from a superclass
       
   442 		// constructor
       
   443 		sorterListener = new RowSorterListener() {
       
   444 		    @Override
       
   445 		    public void sorterChanged(RowSorterEvent e) {
       
   446 			valueCache.clear();
       
   447 
       
   448 			switch (e.getType()) {
       
   449 			case SORT_ORDER_CHANGED:
       
   450 			    break;
       
   451 
       
   452 			default:
       
   453 			case SORTED:
       
   454 			    fitColumnsIfRequested();
       
   455 			}
       
   456 		    }
       
   457 		};
       
   458 
       
   459 		if (oldSorter != null) {
       
   460 		    oldSorter.removeRowSorterListener(sorterListener);
       
   461 		}
       
   462 
       
   463 		if (sorter != null) {
       
   464 		    sorter.addRowSorterListener(sorterListener);
       
   465 		}
       
   466 	    }
       
   467 
       
   468 	    super.setRowSorter(sorter);
       
   469 	    fitColumnsIfRequested();
       
   470 	}
       
   471     }
       
   472 
       
   473     //
       
   474     // ExtTable methods
       
   475     //
       
   476 
       
   477     /**
       
   478      * Calculates and sets the preferred widths for each {@code TableColumn} in
       
   479      * the {@code TableColumnModel}.
       
   480      * <p/>
       
   481      * This operation has a performance cost that increases with the number of
       
   482      * rows or columns in the table, since each cell must be examined to
       
   483      * determine its column's preferred width.	However, this should be a
       
   484      * one-time cost, since these values can be cached for subsequent or {@link
       
   485      * #setAutoFitColumns automatic} calls to this routine.
       
   486      *
       
   487      * @param	    colWidthPolicy
       
   488      *		    how to automatically fit columns
       
   489      *
       
   490      * @param	    useCache
       
   491      *		    {@code true} to cache and reuse the values, {@code false}
       
   492      *		    if this is a one-time operation
       
   493      *
       
   494      * @see	    #setAutoFitColumns
       
   495      */
       
   496     public void fitColumns(ColumnWidthPolicy colWidthPolicy, boolean useCache) {
       
   497 	if (colWidthPolicy == null) {
       
   498 	    return;
       
   499 	}
       
   500 
       
   501 	synchronized (widthCache) {
       
   502 	    TableModel model = getModel();
       
   503 	    TableColumnModel cModel = getColumnModel();
       
   504 	    int cMargin = getColumnModel().getColumnMargin();
       
   505 	    int nvRows = getRowCount();
       
   506 	    int maxColWidth = 0;
       
   507 	    int n = getColumnCount();
       
   508 
       
   509 	    for (int vCol = 0; vCol < n; vCol++) {
       
   510 		int mCol = convertColumnIndexToModel(vCol);
       
   511 		TableColumn column = cModel.getColumn(vCol);
       
   512 
       
   513 		TableCellRenderer renderer = column.getCellRenderer();
       
   514 		if (renderer == null) {
       
   515 		    Class<?> cClass = model.getColumnClass(mCol);
       
   516 		    renderer = getDefaultRenderer(cClass);
       
   517 		}
       
   518 
       
   519 		ArrayList<Integer> colWidthCache = null;
       
   520 
       
   521 		if (useCache) {
       
   522 		    colWidthCache = widthCache.get(column);
       
   523 		    if (colWidthCache == null) {
       
   524 			colWidthCache = new ArrayList<Integer>(0);
       
   525 			colWidthCache.ensureCapacity(nvRows);
       
   526 			for (int i = 0; i < nvRows; i++) {
       
   527 			    colWidthCache.add(-1);
       
   528 			}
       
   529 			widthCache.put(column, colWidthCache);
       
   530 		    }
       
   531 		}
       
   532 
       
   533 		int width = 0;
       
   534 
       
   535 		for (int vRow = 0; vRow < nvRows; vRow++) {
       
   536 		    int mRow = convertRowIndexToModel(vRow);
       
   537 		    int cellWidth = 0;
       
   538 		    try {
       
   539 			cellWidth = colWidthCache == null ?
       
   540 			    -1 : colWidthCache.get(mRow);
       
   541 		    } catch (IndexOutOfBoundsException ignore) {
       
   542 			// This method has probably been called (perhaps by the
       
   543 			// RowSorter) before insertWidthCache could react to a
       
   544 			// tableChange in the TableModel.  That same change will
       
   545 			// ultimately make its way to our own modelListener, and
       
   546 			// this method will be called again (after
       
   547 			// insertWidthCache has been run) so we can abort now.
       
   548 			return;
       
   549 		    }
       
   550 
       
   551 		    if (cellWidth == -1) {
       
   552 			Object value = getValueAt(vRow, vCol);
       
   553 
       
   554 			Component comp = renderer.getTableCellRendererComponent(
       
   555 			    this, value, true, true, vRow, vCol);
       
   556 
       
   557 			cellWidth = comp.getPreferredSize().width;
       
   558 
       
   559 			if (colWidthCache != null) {
       
   560 			    colWidthCache.set(mRow, cellWidth);
       
   561 			}
       
   562 		    }
       
   563 
       
   564 		    width = Math.max(width, cellWidth);
       
   565 		}
       
   566 
       
   567 		// Account for header
       
   568 		renderer = column.getHeaderRenderer();
       
   569 		if (renderer == null) {
       
   570 		    JTableHeader header = getTableHeader();
       
   571 		    if (header != null) {
       
   572 			renderer = header.getDefaultRenderer();
       
   573 		    }
       
   574 		}
       
   575 
       
   576 		if (renderer != null) {
       
   577 		    Object value = column.getHeaderValue();
       
   578 
       
   579 		    Component comp = renderer.getTableCellRendererComponent(
       
   580 			this, value, true, true, -1, vCol);
       
   581 
       
   582 		    int cellWidth = comp.getPreferredSize().width;
       
   583 		    width = Math.max(width, cellWidth);
       
   584 		}
       
   585 
       
   586 		width += cMargin;
       
   587 
       
   588 		maxColWidth = Math.max(maxColWidth, width);
       
   589 		setPreferredWidth(column, width);
       
   590 	    }
       
   591 
       
   592 	    if (colWidthPolicy == ColumnWidthPolicy.EQUAL) {
       
   593 		for (int vCol = 0; vCol < n; vCol++) {
       
   594 		    int mCol = convertColumnIndexToModel(vCol);
       
   595 		    TableColumn column = cModel.getColumn(vCol);
       
   596 		    setPreferredWidth(column, maxColWidth);
       
   597 		}
       
   598 	    }
       
   599 	}
       
   600     }
       
   601 
       
   602     /**
       
   603      * Indicates whether {@link #fitColumns} is called automatically whenever
       
   604      * the table is changed in a significant way.
       
   605      *
       
   606      * @return	    how to automatically fit columns, or {@code null} to not
       
   607      *		    automatically call {@link #fitColumns}
       
   608      */
       
   609     public ColumnWidthPolicy getAutoFitColumns() {
       
   610 	return colWidthPolicy;
       
   611     }
       
   612 
       
   613     /**
       
   614      * Gets the number of rows in the cache of cell values.  A higher value may
       
   615      * speed up rendering of the table if the table model must do expensive
       
   616      * calculations to determine the value in each cell.
       
   617      * <p>
       
   618      * The default value is 0, effectively disabling the cache.
       
   619      */
       
   620     public int getCacheSize() {
       
   621 	return valueCacheSize;
       
   622     }
       
   623 
       
   624     public Color getStripeColor() {
       
   625 	return stripeColor;
       
   626     }
       
   627 
       
   628     /**
       
   629      * Gets the number of rows used to calculate the preferred size of an
       
   630      * enclosing viewport.
       
   631      *
       
   632      * @return	    the number of rows to show in an enclosing viewport, or -1
       
   633      *		    to rely on the default calculation for preferred viewport
       
   634      *		    size
       
   635      */
       
   636     public int getVisibleRowCount() {
       
   637 	return visibleRowCount;
       
   638     }
       
   639 
       
   640     /**
       
   641      * Indicates whether this table has a non-{@code null} {@link
       
   642      * #getStripeColor stripe color}.
       
   643      */
       
   644     public boolean isStriped() {
       
   645 	return getStripeColor() != null;
       
   646     }
       
   647 
       
   648     /**
       
   649      * Sets whether {@link #fitColumns} is called automatically whenever the
       
   650      * table is changed in a significant way.
       
   651      *
       
   652      * @param	    colWidthPolicy
       
   653      *		    how to automatically fit columns, or {@code null} to not
       
   654      *		    automatically call {@link #fitColumns}
       
   655      */
       
   656     public void setAutoFitColumns(ColumnWidthPolicy colWidthPolicy) {
       
   657 	if (this.colWidthPolicy != colWidthPolicy) {
       
   658 	    this.colWidthPolicy = colWidthPolicy;
       
   659 	    fitColumnsIfRequested();
       
   660 	}
       
   661     }
       
   662 
       
   663     /**
       
   664      * Sets the number of rows in the cache of cell values.  See {@link
       
   665      * #getCacheSize}.
       
   666      */
       
   667     public void setCacheSize(int valueCacheSize) {
       
   668 	if (this.valueCacheSize != valueCacheSize) {
       
   669 	    if (this.valueCacheSize > valueCacheSize) {
       
   670 		try {
       
   671 		    // Trim if necessary
       
   672 		    ListIterator<CachedRow<Object>> rowIter =
       
   673 			valueCache.listIterator(valueCacheSize);
       
   674 
       
   675 		    while (rowIter.hasNext()) {
       
   676 			rowIter.next();
       
   677 			rowIter.remove();
       
   678 		    }
       
   679 		} catch (IndexOutOfBoundsException ignore) {
       
   680 		}
       
   681 	    }
       
   682 
       
   683 	    this.valueCacheSize = valueCacheSize;
       
   684 	}
       
   685     }
       
   686 
       
   687     public void setStripeColor(Color stripeColor) {
       
   688 	if (this.stripeColor != stripeColor) {
       
   689 	    this.stripeColor = stripeColor;
       
   690 	    repaint();
       
   691 	}
       
   692     }
       
   693 
       
   694     /**
       
   695      * Sets the number of rows used to calculate the preferred size of an
       
   696      * enclosing viewport.
       
   697      *
       
   698      * @param	    visibleRowCount
       
   699      *		    the number of rows to show in an enclosing viewport, or -1
       
   700      *		    to rely on the default calculation for preferred viewport
       
   701      *		    size
       
   702      */
       
   703     public void setVisibleRowCount(int visibleRowCount) {
       
   704 	this.visibleRowCount = visibleRowCount;
       
   705     }
       
   706 
       
   707     //
       
   708     // Private methods
       
   709     //
       
   710 
       
   711     private void init() {
       
   712 	setTableHeader(new ExtTableHeader(getColumnModel()));
       
   713 	setFillsViewportHeight(true);
       
   714 
       
   715 	addMouseListener(
       
   716 	    new MouseAdapter() {
       
   717 		@Override
       
   718 		public void mousePressed(MouseEvent e) {
       
   719 		    int vRow = rowAtPoint(e.getPoint());
       
   720 		    if (vRow == -1) {
       
   721 			clearSelection();
       
   722 		    }
       
   723 		}
       
   724 	    });
       
   725 
       
   726 	setShowHorizontalLines(false);
       
   727 	setShowVerticalLines(true);
       
   728 
       
   729 	// Accommodate a 16-pixel icon + a 1-pixel focus border
       
   730 	setRowHeight(18);
       
   731     }
       
   732 
       
   733     private void clearWidthCache() {
       
   734 	if (widthCache != null) {
       
   735 	    synchronized (widthCache) {
       
   736 		widthCache.clear();
       
   737 	    }
       
   738 	}
       
   739     }
       
   740 
       
   741     private void deleteUnusedColumnsFromWidthCache() {
       
   742 	TableColumnModel cModel = getColumnModel();
       
   743 	synchronized (widthCache) {
       
   744 	    CACHED: for (Iterator<TableColumn> i =
       
   745 		widthCache.keySet().iterator(); i.hasNext();) {
       
   746 
       
   747 		TableColumn column = i.next();
       
   748 		for (int j = 0, n = cModel.getColumnCount(); j < n; j++) {
       
   749 		    if (cModel.getColumn(j) == column) {
       
   750 			continue CACHED;
       
   751 		    }
       
   752 		}
       
   753 		i.remove();
       
   754 	    }
       
   755 	}
       
   756     }
       
   757 
       
   758     private void deleteWidthCache(int fmRow, int lmRow) {
       
   759 	synchronized (widthCache) {
       
   760 	    for (TableColumn column : widthCache.keySet()) {
       
   761 		ArrayList<Integer> colWidthCache = widthCache.get(column);
       
   762 		for (int mRow = lmRow; mRow >= fmRow; mRow--) {
       
   763 		    colWidthCache.remove(mRow);
       
   764 		}
       
   765 	    }
       
   766 	}
       
   767     }
       
   768 
       
   769     private void fitColumnsIfRequested() {
       
   770 	if (colWidthPolicy != null) {
       
   771 	    fitColumns(colWidthPolicy, true);
       
   772 	}
       
   773     }
       
   774 
       
   775     private void insertWidthCache(int fmRow, int lmRow) {
       
   776 	synchronized (widthCache) {
       
   777 	    for (TableColumn column : widthCache.keySet()) {
       
   778 		ArrayList<Integer> colWidthCache = widthCache.get(column);
       
   779 		for (int mRow = fmRow; mRow <= lmRow; mRow++) {
       
   780 		    colWidthCache.add(mRow, -1);
       
   781 		}
       
   782 	    }
       
   783 	}
       
   784     }
       
   785 
       
   786     private void resetWidthCache(int fmRow, int lmRow, int fmCol, int lmCol) {
       
   787 	synchronized (widthCache) {
       
   788 	    TableColumnModel cModel = getColumnModel();
       
   789 
       
   790 	    for (int mCol = fmCol; mCol <= lmCol; mCol++) {
       
   791 		int vCol = convertColumnIndexToView(mCol);
       
   792 		TableColumn column = cModel.getColumn(vCol);
       
   793 
       
   794 		ArrayList<Integer> colWidthCache = widthCache.get(column);
       
   795 		if (colWidthCache != null) {
       
   796 		    for (int mRow = fmRow; mRow <= lmRow; mRow++) {
       
   797 			try {
       
   798 			    colWidthCache.set(mRow, -1);
       
   799 			} catch (IndexOutOfBoundsException ignore) {
       
   800 			}
       
   801 		    }
       
   802 		}
       
   803 	    }
       
   804 	}
       
   805     }
       
   806 
       
   807     private void setPreferredWidth(TableColumn column, int width) {
       
   808 	column.setPreferredWidth(width);
       
   809 	column.setMinWidth(Math.min(column.getMinWidth(), width));
       
   810     }
       
   811 
       
   812     private void updateWidthCache(TableModelEvent e) {
       
   813 	synchronized (widthCache) {
       
   814 	    int fmRow = e.getFirstRow();
       
   815 	    int lmRow = e.getLastRow();
       
   816 
       
   817 	    int nRows = getModel().getRowCount();
       
   818 	    if (lmRow >= nRows) {
       
   819 		lmRow = nRows - 1;
       
   820 	    }
       
   821 
       
   822 	    switch (e.getType()) {
       
   823 	    case TableModelEvent.UPDATE:
       
   824 		int fmCol = e.getColumn();
       
   825 		int lmCol = fmCol;
       
   826 
       
   827 		if (fmCol == TableModelEvent.ALL_COLUMNS) {
       
   828 		    fmCol = 0;
       
   829 		    lmCol = getColumnModel().getColumnCount() - 1;
       
   830 		}
       
   831 
       
   832 		resetWidthCache(fmRow, lmRow, fmCol, lmCol);
       
   833 		break;
       
   834 
       
   835 
       
   836 	    case TableModelEvent.DELETE:
       
   837 		deleteWidthCache(fmRow, lmRow);
       
   838 		break;
       
   839 
       
   840 	    case TableModelEvent.INSERT:
       
   841 		insertWidthCache(fmRow, lmRow);
       
   842 		break;
       
   843 	    }
       
   844 
       
   845 	    fitColumnsIfRequested();
       
   846 	}
       
   847     }
       
   848 
       
   849     //
       
   850     // Static methods
       
   851     //
       
   852 
       
   853     // XXX Unit test -- remove
       
   854     public static void main(String[] args) {
       
   855 	JFrame f = new JFrame();
       
   856 
       
   857 	JPanel c = (JPanel)f.getContentPane();
       
   858 	c.setLayout(new BorderLayout());
       
   859 	c.setBorder(GUIUtil.getEmptyBorder());
       
   860 
       
   861 	Object[][] data = {
       
   862 	    new Object[] {"0 zero", "0 zero", "blah"},
       
   863 	    new Object[] {"1 one", "1 one", "blah"},
       
   864 	    new Object[] {"2 two", "2 two", "blah"},
       
   865 	    new Object[] {"3 three", "3 three", "blah"},
       
   866 	    new Object[] {"4 four", "4 four", "blah"},
       
   867 	    new Object[] {"5 five", "5 five", "blah"},
       
   868 	};
       
   869 
       
   870 	final DefaultTableModel model = new DefaultTableModel(
       
   871 	    data, new String[] {"Column 0", "Column 1", "Column 2"});
       
   872 
       
   873 	final ExtTable t = new ExtTable(model);
       
   874 	t.setAutoFitColumns(ColumnWidthPolicy.PREFERRED);
       
   875 //	t.setOpaque(false);
       
   876 //	t.setBackground(Color.green);
       
   877 //	t.setStripeColor(null);
       
   878 //	t.setShowGrid(true);
       
   879 
       
   880 	t.addMouseListener(
       
   881 	    new MouseAdapter() {
       
   882 		@Override
       
   883 		public void mouseClicked(MouseEvent e) {
       
   884 		    if (e.getButton() == 3) {
       
   885 			Point p = e.getPoint();
       
   886 			int vCol = t.columnAtPoint(p);
       
   887 			int mCol = t.convertColumnIndexToModel(vCol);
       
   888 
       
   889 			Object[] row = {"new", "new", "new"};
       
   890 			row[mCol] = "loooooooooooooooooooooooooong";
       
   891 			model.addRow(row);
       
   892 		    } else
       
   893 
       
   894 		    if (e.getButton() == 2) {
       
   895 			Point p = e.getPoint();
       
   896 			int vRow = t.rowAtPoint(p);
       
   897 			int mRow = t.convertRowIndexToModel(vRow);
       
   898 			model.removeRow(mRow);
       
   899 		    }
       
   900 		}
       
   901 	    });
       
   902 
       
   903 	JScrollPane scroll = new ExtScrollPane(t);
       
   904 	c.add(scroll, BorderLayout.CENTER);
       
   905 
       
   906 	JButton b = new JButton("Remove column");
       
   907 	b.addActionListener(
       
   908 	    new ActionListener() {
       
   909 		@Override
       
   910 		public void actionPerformed(ActionEvent e) {
       
   911 		    TableColumn column = t.getColumnModel().getColumn(1);
       
   912 		    System.out.printf("Removing column: %s\n",
       
   913 			column.getIdentifier());
       
   914 		    t.removeColumn(column);
       
   915 		}
       
   916 	    });
       
   917 	c.add(b, BorderLayout.SOUTH);
       
   918 
       
   919 	f.pack();
       
   920 	f.setVisible(true);
       
   921     }
       
   922 }