package com.limegroup.gnutella.gui.tables; import java.awt.Color; import java.awt.Component; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.Arrays; import java.util.Collections; import java.util.EventListener; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.swing.JComponent; import javax.swing.JTable; import javax.swing.JToolTip; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.Border; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import javax.swing.text.Position; import com.limegroup.gnutella.gui.GUIUtils; import com.limegroup.gnutella.gui.JMultilineToolTip; import com.limegroup.gnutella.gui.themes.ThemeFileHandler; import com.limegroup.gnutella.util.DataUtils; /** * A specialized JTable for use with special Lime functions. * 1) Allows the user to easily * set a column as visible or invisible, rather than * having to remove/add columns. * It internally will remember where the column was and * add/remove it as needed. * 2) It remembers which column is sorted and whether the sort * is ascending or descending. * For use with adding arrows to the tableHeader. * 3) Shows special tooltips for each row. * @author Sam Berlin */ public final class LimeJTable extends JTable implements JSortTable { /** * The columns that are currently hidden. */ protected Map /* of String -> LimeTableColumn */ _hiddenColumns = new HashMap(); /** * The index of the column that is currently pressed down. */ protected int pressedColumnIndex = -1; /** * The array of tooltip data to display next. */ private String[] tips; /** * The array to use when the tip is for extending a clipped name. */ private final String[] CLIPPED_TIP = new String[1]; /** * The last LimeTableColumn that was removed from this table. */ private static LimeTableColumn _lastRemoved; /** * The preferences handler for the table columns. */ protected ColumnPreferenceHandler columnPreferences; /** * The settings for this table. */ protected TableSettings tableSettings; /** * Whether or not mouse events are being proxied. */ protected boolean mouseEventsProxied = false; /** * Same as JTable(). * The table MUST have setModel called with a DataLineModel in order * for this class to function properly. */ public LimeJTable() { super(); setToolTipText(""); GUIUtils.fixInputMap(this); addFocusListener(FocusHandler.INSTANCE); } /** * Same as JTable(DataLineModel) */ public LimeJTable(DataLineModel dm) { super(dm); setToolTipText(""); GUIUtils.fixInputMap(this); addFocusListener(FocusHandler.INSTANCE); } /** * Overriden to not manage focus. * (Other it causes some problems in search results) */ public boolean isManagingFocus() { return false; } /** * Sets the given row to be the only one selected. */ public void setSelectedRow(int row) { clearSelection(); addRowSelectionInterval(row, row); } /** * Gets the selected DataLine (or null if none) */ public DataLine getSelectedDataLine() { int selected = getSelectedRow(); if(selected != -1) return ((DataLineModel)dataModel).get(selected); else return null; } /** * Override getSelectedRow to ensure that it exists in the table. * This is necessary because of bug 4730055. * See: http://developer.java.sun.com/developer/bugParade/bugs/4730055.html */ public int getSelectedRow() { int selected = super.getSelectedRow(); if( selected >= dataModel.getRowCount() ) return -1; else return selected; } /** * Gets all selected DataLines (returns an empty array if none * are selected. */ public DataLine[] getSelectedDataLines() { int selected[] = getSelectedRows(); if(selected == null || selected.length == 0) return new DataLine[0]; DataLine[] ret = new DataLine[selected.length]; for(int i = 0; i < ret.length; i++) ret[i] = ((DataLineModel)dataModel).get(selected[i]); return ret; } /** * Overrided getSelectedRows to ensure that all selected rows exist in the * table. This is necessary because of bug 4730055. * See: http://developer.java.sun.com/developer/bugParade/bugs/4730055.html * * As a side effect, this implementation will return the rows in a sorted * order. (Lowest first) */ public int[] getSelectedRows() { int[] selected = super.getSelectedRows(); if( selected == null || selected.length == 0) return selected; Arrays.sort(selected); int tableSize = dataModel.getRowCount(); for(int i = 0; i < selected.length; i++) { // Short-circuit when we find an invalid value. if( selected[i] >= tableSize ) { int[] newData = new int[i]; System.arraycopy(selected, 0, newData, 0, i); return newData; } } //Nothing was outside of the selection range. return selected; } /** * Ensures the selected row is visible. */ public void ensureSelectionVisible() { ensureRowVisible(getSelectedRow()); } /** * Ensures the given row is visible. */ public void ensureRowVisible(int row) { if(row != -1) { Rectangle cellRect = getCellRect(row, 0, false); Rectangle visibleRect = getVisibleRect(); if( !visibleRect.intersects(cellRect) ) scrollRectToVisible(cellRect); } } /** * Determines if the selected row is visible. */ public boolean isSelectionVisible() { return isRowVisible(getSelectedRow()); } /** * Determines if the given row is visible. */ public boolean isRowVisible(int row) { if(row != -1) { Rectangle cellRect = getCellRect(row, 0, false); Rectangle visibleRect = getVisibleRect(); return visibleRect.intersects(cellRect); } else return false; } /** * Access the ColumnPreferenceHandler. */ public ColumnPreferenceHandler getColumnPreferenceHandler() { return columnPreferences; } /** * Set the ColumnPreferenceHandler */ public void setColumnPreferenceHandler(ColumnPreferenceHandler handl) { columnPreferences = handl; } /** * Access the TableSettings. */ public TableSettings getTableSettings() { return tableSettings; } /** * Set the TableSettings. */ public void setTableSettings(TableSettings settings) { tableSettings = settings; } /** * set the pressed header column. * @param col The MODEL index of the column */ public void setPressedColumnIndex(int col) { pressedColumnIndex = col; } /** * get the pressed header column * @return the VIEW index of the pressed column. */ public int getPressedColumnIndex() { return convertColumnIndexToView(pressedColumnIndex); } /** * @return the VIEW index of the sorted column. */ public int getSortedColumnIndex() { return convertColumnIndexToView( ((DataLineModel)dataModel).getSortColumn() ); } /** * accessor function */ public boolean isSortedColumnAscending() { return ((DataLineModel)dataModel).isSortAscending(); } /** * Simple function that tucks away hidden columns for use later. * And it uses them later! */ public void setColumnVisible(Object columnId, boolean visible) throws LastColumnException { if ( !visible ) { TableColumnModel model = getColumnModel(); // don't allow the last column to be removed. if ( model.getColumnCount() == 1 ) throw new LastColumnException(); TableColumn column = model.getColumn( model.getColumnIndex(columnId) ); _hiddenColumns.put( columnId, column ); _lastRemoved = (LimeTableColumn)column; removeColumn(column); } else { TableColumn column = (TableColumn)_hiddenColumns.get(columnId); _hiddenColumns.remove( columnId ); addColumn(column); } } /** * Returns an iterator of the removed columns. */ public Iterator getHiddenColumns() { return Collections.unmodifiableCollection( _hiddenColumns.values()).iterator(); } /** * Returns the last removed column. */ public LimeTableColumn getLastRemovedColumn() { return _lastRemoved; } /** * Determines whether or not a column is visible in this table. */ public boolean isColumnVisible(Object columnId) { return !_hiddenColumns.containsKey(columnId); } /** * Determines if the given point is a selected row. */ public boolean isPointSelected(Point p) { int row = rowAtPoint(p); int col = columnAtPoint(p); if(row == -1 || col == -1) return false; int sel[] = getSelectedRows(); for(int i = 0; i < sel.length; i++) if(sel[i] == row) return true; return false; } /** * Determines whether or not mouse events are being proxied. */ public boolean isMouseEventsProxied() { return mouseEventsProxied; } /** * Sets whether or not mouse events are being proxied. */ public void setMouseEventsProxied(boolean proxy) { mouseEventsProxied = proxy; } /** * Processes the given mouse event. */ public void processMouseEvent(MouseEvent e) { try { super.processMouseEvent(e); // deselect rows if if (e.getID() == MouseEvent.MOUSE_CLICKED && SwingUtilities.isLeftMouseButton(e)) { TableModel model = getModel(); if (model != null) { int index = rowAtPoint(e.getPoint()); if (index < 0 || index >= model.getRowCount()) { clearSelection(); } } } } catch(ArrayIndexOutOfBoundsException aioobe) { // A bug in Java 1.3 causes an AIOOBE from PopupMenus. // Normally we would ignore this, since it has nothing // to do with LimeWire -- but because we insert ourselves // into the call-chain here, we must manually ignore the error. String msg = aioobe.getMessage(); if(msg != null && msg.indexOf("at javax.swing.MenuSelectionManager.processMouseEvent") != -1) return; // ignore; throw aioobe; } } /** * Removes the given MouseListener. */ public void removeMouseListener(MouseListener ml) { if(!mouseEventsProxied) { super.removeMouseListener(ml); return; } EventListener[] oldListeners = getListeners(MouseListener.class); super.removeMouseListener(ml); EventListener[] newListeners = getListeners(MouseListener.class); // if nothing removed, see if we can remove the MouseListener proxy. if(oldListeners.length == newListeners.length) { for(int i = 0; i < oldListeners.length; i++) { EventListener current = oldListeners[i]; if(current instanceof EventListenerProxy) { if(((EventListenerProxy)current).isProxyFor(ml)) { super.removeMouseListener((MouseListener)current); return; } } } } } /** * Removes the given MouseMotionListener. */ public void removeMouseMotionListener(MouseMotionListener ml) { if(!mouseEventsProxied) { super.removeMouseMotionListener(ml); return; } EventListener[] oldListeners = getListeners(MouseMotionListener.class); super.removeMouseMotionListener(ml); EventListener[] newListeners = getListeners(MouseMotionListener.class); // if nothing removed, see if we can remove MouseMotionListener proxy. if(oldListeners.length == newListeners.length) { for(int i = 0; i < oldListeners.length; i++) { EventListener current = oldListeners[i]; if(current instanceof EventListenerProxy) { if(((EventListenerProxy)current).isProxyFor(ml)) { super.removeMouseMotionListener((MouseMotionListener)current); return; } } } } } /** * Sets the internal tooltip text for use with the next * createToolTip. */ public String getToolTipText(MouseEvent e) { Point p = e.getPoint(); int row = rowAtPoint(p); int col = columnAtPoint(p); int colModel = convertColumnIndexToModel(col); DataLineModel dlm = (DataLineModel)dataModel; boolean isClippable = col > -1 && row > -1 ? dlm.isClippable(colModel) : false; boolean forceTooltip = col > -1 && row > -1 ? dlm.isTooltipRequired(row, col) : false; // If the user doesn't want tooltips, only display // them if the column is too small (and data is clipped) if (!tableSettings.DISPLAY_TOOLTIPS.getValue() && !forceTooltip) { if (isClippable) return clippedToolTip(row, col, colModel); else return null; } if ( row > -1 ) { //set the internal tips for later use with createToolTip tips = dlm.getToolTipArray(row, colModel); // NOTE: the below return triggers the tooltip manager to // create a tooltip. // If it is null, one won't be created. // If two different rows return the same tip, the manager // won't be triggered to create a 'new' tip. // Rather than return the actual row#, which could stay the same // if sorting is enabled & the DataLine moves, // return the string representation // of the dataline, so if the row moves out from under the mouse, // the tooltip will auto change when the mouse // moves around the new DataLine (same row) if (tips == null) { // if we're over a column, see if we can display a clipped tool tip. if (isClippable) return clippedToolTip(row, col, colModel); else return null; } else return dlm.get(row).toString() + col; } tips = DataUtils.EMPTY_STRING_ARRAY; return null; } /** * Displays a tooltip for clipped data, if possible. * * @param row the row of the data * @param col the VIEW index of the column * @param colModel the MODEL index of the column */ private String clippedToolTip(int row, int col, int colModel) { TableColumn tc = getColumnModel().getColumn(col); int columnWidth = tc.getWidth(); int dataWidth = getDataWidth(row, colModel); if (columnWidth < dataWidth) { tips = CLIPPED_TIP; return ((DataLineModel)dataModel).get(row).toString() + col; } else { tips = DataUtils.EMPTY_STRING_ARRAY; return null; } } /** * Gets the width of the data in the specified row/column. * * @param row the row of the data * @param col the MODEL index of the column */ private int getDataWidth(int row, int col) { DataLineModel dlm = (DataLineModel)dataModel; DataLine dl = dlm.get(row); Object data = dl.getValueAt(col); String info; if( data != null && (info = data.toString()) != null ) { CLIPPED_TIP[0] = info; TableCellRenderer tcr = getDefaultRenderer(dlm.getColumnClass(col)); JComponent renderer = (JComponent)tcr.getTableCellRendererComponent( this, data, false, false, row, col); try { FontMetrics fm = renderer.getFontMetrics(renderer.getFont()); return fm.stringWidth(info) + 3; } catch (NullPointerException npe) { return -1; } } else { return -1; } } /** *@return The JToolTip returned is actually a JMultilineToolTip */ public JToolTip createToolTip() { JMultilineToolTip ret = JMultilineToolTip.instance(); ret.setToolTipArray( tips ); tips = DataUtils.EMPTY_STRING_ARRAY; return ret; } /** * Overrides JTable's default implementation in order to add * LimeTableColumn columns. */ public void createDefaultColumnsFromModel() { DataLineModel dlm = (DataLineModel)dataModel; if (dlm != null) { // Remove any current columns TableColumnModel cm = getColumnModel(); while (cm.getColumnCount() > 0) { cm.removeColumn(cm.getColumn(0)); } // Create new columns from the data model info for (int i = 0; i < dlm.getColumnCount(); i++) { TableColumn newColumn = dlm.getTableColumn(i); addColumn(newColumn); } } } /** * Returns the color that a specific row will be. */ public Color getBackgroundForRow(int row) { if(row % 2 == 0 || !tableSettings.ROWSTRIPE.getValue()) return getBackground(); else return ThemeFileHandler.TABLE_ALTERNATE_COLOR.getValue(); } /** * This overrides JTable.prepareRenderer so that we can stripe the * rows as needed. */ public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { if(renderer == null) throw new IllegalStateException("null renderer, row: " + row + ", column: " + column + ", id: " + tableSettings.getID() + ", columnId: " + getColumnModel().getColumn(column)); Object value = getValueAt(row, column); boolean isSelected = isCellSelected(row, column); boolean rowIsAnchor = selectionModel.getAnchorSelectionIndex() == row; boolean colIsAnchor = columnModel.getSelectionModel().getAnchorSelectionIndex() == column; boolean hasFocus = rowIsAnchor && colIsAnchor && hasFocus(); Component r = renderer.getTableCellRendererComponent(this, value, isSelected, hasFocus, row, column); Color odd = ThemeFileHandler.TABLE_ALTERNATE_COLOR.getValue(); Color even = ThemeFileHandler.TABLE_BACKGROUND_COLOR.getValue(); if ( isSelected ) { // do nothing if selected. } else if (hasFocus && isCellEditable(row, column)) { // do nothing if we're focused & editting. } else if (even.equals(odd)) { // do nothing if backgrounds are the same. } else if (!tableSettings.ROWSTRIPE.getValue()) { // if the renderer's background isn't already the normal one, // change it. (needed for real-time changing of the option) if( r != null && !r.equals(even) ) r.setBackground(even); } else if ( row % 2 != 0 ) { r.setBackground(odd); } else { r.setBackground(even); } return r; } /** * Returns the next list element that starts with * a prefix. * * @param prefix the string to test for a match * @param startIndex the index for starting the search * @param bias the search direction, either * Position.Bias.Forward or Position.Bias.Backward. * @return the index of the next list element that * starts with the prefix; otherwise -1 * @exception IllegalArgumentException if prefix is null * or startIndex is out of bounds */ public int getNextMatch(String prefix, int startIndex, Position.Bias bias) { DataLineModel model = (DataLineModel)dataModel; int max = model.getRowCount(); if (prefix == null) throw new IllegalArgumentException(); if (startIndex < 0 || startIndex >= max) throw new IllegalArgumentException(); prefix = prefix.toUpperCase(); // start search from the next element after the selected element int increment = (bias == Position.Bias.Forward) ? 1 : -1; int index = startIndex; int typeAheadColumn = model.getTypeAheadColumn(); if(typeAheadColumn >= 0 && typeAheadColumn < model.getColumnCount()) { do { Object o = model.getValueAt(index, typeAheadColumn); if (o != null) { String string; if (o instanceof String) string = ((String)o).toUpperCase(); else { string = o.toString(); if (string != null) string = string.toUpperCase(); } if (string != null && string.startsWith(prefix)) return index; } index = (index + increment + max) % max; } while (index != startIndex); } return -1; } /* * Stretch JTable to JViewport height so that the space * underneath the rows fires mouse events as well */ public boolean getScrollableTracksViewportHeight() { Component parent = getParent(); if (parent instanceof javax.swing.JViewport) return parent.getHeight() > getPreferredSize().height; return super.getScrollableTracksViewportHeight(); } /** * Paints the table & a focused row border. */ public void paint(Graphics g) { super.paint(g); int focusedRow = getFocusedRow(true); if(focusedRow != -1 && focusedRow < getRowCount() ) { Border rowBorder = UIManager.getBorder("Table.focusRowHighlightBorder"); if(rowBorder != null) { Rectangle rect = getCellRect(focusedRow, 0, true); rect.width = getWidth(); rowBorder.paintBorder(this, g, rect.x, rect.y, rect.width, rect.height); } } } /** * Repaints the focused row if one was focused. */ private void repaintFocusedRow() { int focusedRow = getFocusedRow(false); if(focusedRow != -1 && focusedRow < getRowCount()) { Rectangle rect = getCellRect(focusedRow, 0, true); rect.width = getWidth(); repaint(rect); } } /** * Gets the focused row. */ private int getFocusedRow(boolean requireFocus) { if(!requireFocus || hasFocus()) return selectionModel.getAnchorSelectionIndex(); else return -1; } /** * Handler for repainting focus for all tables. */ private static class FocusHandler implements FocusListener { private static final FocusListener INSTANCE = new FocusHandler(); public void focusGained(FocusEvent e) { LimeJTable t = (LimeJTable)e.getSource(); t.repaintFocusedRow(); } public void focusLost(FocusEvent e) { LimeJTable t = (LimeJTable)e.getSource(); t.repaintFocusedRow(); } } }