/* SmartTable.java This Module encapsulates user interactions with a standard Swing JTable, including right click menus to sort and remove columns Created: 14 December 2005 Module By: James Ratcliff, falazar@arlut.utexas.edu ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2013 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.JTable; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.print.PrinterException; import java.awt.print.Printable; import java.awt.print.PrinterJob; import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Vector; import javax.print.attribute.HashPrintRequestAttributeSet; import javax.print.attribute.PrintRequestAttributeSet; import javax.swing.event.AncestorListener; import javax.swing.event.AncestorEvent; import javax.swing.event.ChangeEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableColumnModelListener; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.JTableHeader; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import arlut.csd.Util.TranslationService; /*------------------------------------------------------------------------------ class SmartTable ------------------------------------------------------------------------------*/ /** * <p>Extending upon the standard Swing JTable, adding the ability to * Sort Columns with the TableSorter Class, Adding Right click context * menu's for the header row and the data rows. Re-created a Optimize * Column Widths with the TextAreaRenderer class to make each cell a * TextArea.</p> */ public class SmartTable extends JPanel implements ActionListener { static final boolean debug = false; /** * TranslationService object for handling string localization in the * Ganymede client. */ static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.JTable.SmartTable"); /** * Localized date pattern for rendering dates in the table. */ static final String datePattern = SmartTable.ts.l("getTableCellRendererComponent.datePattern"); // "M/d/yyyy" // --- /** * The GUI table component. */ private JTable table = null; private MyTableModel myModel; private TableSorter sorter = null; /** * Hashable index for selecting rows by key field */ private Map<Object, Integer> index; /** * The callback we'll send menu activity notifications to. */ private rowSelectCallback callback; // Header Menus for right click popup private JMenuItem menuTitle = new JMenuItem(ts.l("init.menu_title")); // "Column Menu" private JMenuItem deleteColMI = new JMenuItem(ts.l("init.del_col")); // "Delete This Column" private JMenuItem optimizeColWidMI = new JMenuItem(ts.l("init.opt_col_widths"));// "Optimize Column Widths" // vars to remember to pass into the mouse actions private int remember_row; private int remember_col; private int remember_col2; /* -- */ /** * SmartTable constructor * * @param columnValues The name of the columns to be held in the table. * @param rowMenu A popup menu to associate with each row of the table. * @param callback The arl.csd.JTable-specific rowSelectCallback listener that receives * notification of events on the table rows. */ public SmartTable(String[] columnValues, JPopupMenu rowMenu, rowSelectCallback callback) { if (debug) { System.err.println("DEBUG: SmartTable Constructor"); } this.setLayout(new BorderLayout()); index = new HashMap<Object, Integer>(); this.callback = callback; myModel = new MyTableModel(columnValues); sorter = new TableSorter(myModel); table = new JTable(sorter); table.setTableHeader(new MyJTableHeader(table.getColumnModel())); sorter.setTableHeader(table.getTableHeader()); table.setPreferredScrollableViewportSize(new Dimension(500, 70)); // Allows horizontal scrolling - Lets all cols be about 100 px as opposed to fitting panel table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // Fix to display dates nicely table.setDefaultRenderer(Date.class, new DateCellRenderer()); addPopupMenus(rowMenu); // Sort Function Call Here - default first field ASC sorter.setSortingStatus(0, 1); // We're going to reflow columns whenever they are resized table.getTableHeader().getColumnModel().addColumnModelListener(new ColumnReflowListener()); JScrollPane scrollPane = new JScrollPane(table); this.add(scrollPane); table.addAncestorListener(new SmartTableAncestorListener()); this.addComponentListener(new SmartTableComponentListener()); } /** * Add right click menus for header, and row. * * @param rowMenu popup menu passed in from parent, actionListener added here */ private void addPopupMenus(JPopupMenu rowMenu) { JPopupMenu headerMenu = new JPopupMenu(); headerMenu.add(menuTitle); headerMenu.addSeparator(); headerMenu.add(deleteColMI); headerMenu.add(optimizeColWidMI); deleteColMI.addActionListener(this); optimizeColWidMI.addActionListener(this); table.getTableHeader().addMouseListener(new PopupListener(this, headerMenu)); if (rowMenu != null) { for (Component element: rowMenu.getComponents()) { if (element instanceof JMenuItem) { JMenuItem temp = (JMenuItem) element; // if there is a listener already, dont add another // one if (temp.getActionListeners().length == 0) { temp.addActionListener(this); } } } } table.addMouseListener(new PopupListener(this,rowMenu)); } public int getColumnCount() { return myModel.getColumnCount(); } public String getColumnName(int j) { return myModel.getColumnName(j); } public int getRowCount() { return myModel.getRowCount(); } public Object getValueAt(int i, int j) { return myModel.getValueAt(i, j); } /** * Function for the Toolbar, Rightclick Row Menus, called from * popuplistener */ public void actionPerformed(ActionEvent event) { if (event.getSource() instanceof JMenuItem) { JMenuItem eventSource = (JMenuItem) event.getSource(); Container parentContainer = eventSource.getParent(); if (parentContainer instanceof JPopupMenu) { if (event.getSource() == deleteColMI) { if (debug) { System.out.println("mouseevent remove col:" + remember_col2 + "*"); } table.removeColumn(table.getColumnModel().getColumn(remember_col2)); calcResizeMode(); } else if (event.getSource() == optimizeColWidMI) { if (debug) { System.out.println("mouseevent optimize all columns "); } optimizeCols(); } else // pass back to parent to deal with Row menu actions { Object key = getRowKey(sorter.modelIndex(remember_row)); // get real key from sorter model if (debug) { System.err.println("actionPerformed processing hash key: row=" + remember_row + ", invid=" + key); } callback.rowMenuPerformed(key, event); } } } } /** * Modify the table autoresize mode according to whether there is * room for all the columns or not. */ public void calcResizeMode() { int colCount = table.getColumnCount(); if (colCount == 0) { return; } // default width is 75, if not default, use getPreferredWidth() int colWidth = table.getColumnModel().getColumn(0).getPreferredWidth(); // Get Table Size, then get Container size, if table smaller than // container, stretch table out to fit if (colWidth*colCount < table.getParent().getWidth()) { table.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN); } else { table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); } } /** * <p>This method will go through all of the columns and optimize * the pole placement to minimize wasted space and provide a decent * balance of row and column sizes.</p> * * <p>Somehow.</p> */ public synchronized void optimizeCols() { if (debug) { System.err.println("baseTable.optimizeCols(): entering"); } int nominalWidth[]; float totalOver, spareSpace; float percentSpace, shrinkFactor, percentOver, growthFactor; float redistribute = (float) 0.0; /* -- */ // Set Each Column to Auto-Wrap text if needed Enumeration<TableColumn> columns = table.getColumnModel().getColumns(); while (columns.hasMoreElements()) { TableColumn col = columns.nextElement(); reflowColumn(col); } /* This method uses the following variables to do its calculations. nominalWidth[] - An array of ints holding the width needed by each column. totalOver - the aggregate amount of horizontal space that the columns are short, in the absence of wordwrapping. spareSpace - the aggregate amount of horizontal space that the columns have to spare. */ if (debug) { System.err.println("this.getBounds().width = " + this.getBounds().width); } nominalWidth = new int[table.getColumnCount()]; totalOver = (float) 0.0; spareSpace = (float) 0.0; columns = table.getColumnModel().getColumns(); int i = -1; while (columns.hasMoreElements()) { TableColumn col = columns.nextElement(); i++; if (debug) { System.err.println("Examining column " + i); } TextAreaRenderer renderer = (TextAreaRenderer) col.getCellRenderer(); if (renderer == null) { continue; } nominalWidth[i] = 20; for (int j = 0; j < myModel.getRowCount(); j++) { Object value = myModel.getValueAt(j, col.getModelIndex()); int localNW = renderer.getUnwrappedWidth(this.table, value) + 5; if (localNW > nominalWidth[i]) { nominalWidth[i] = localNW; } } if (debug) { System.err.println(); } // nominalWidth[i] is now the required width of this column if (debug) { System.err.println("Column " + i + " has nominalWidth of " + nominalWidth[i] + " and a cellWidth of " + col.getWidth()); } if (nominalWidth[i] < col.getWidth()) { spareSpace += col.getWidth() - nominalWidth[i]; } else { totalOver += (float) nominalWidth[i] - col.getWidth(); } } if (debug) { System.err.println("spareSpace = " + spareSpace + ", totalOver = " + totalOver); } redistribute = java.lang.Math.min(spareSpace, totalOver); if (debug) { System.err.println("redistribute = " + redistribute); } columns = table.getColumnModel().getColumns(); i = -1; while (columns.hasMoreElements()) { TableColumn col = columns.nextElement(); i++; // are we going to be actually doing some redistributing? if (redistribute > 1.0) { // Does this column have space to give? if (nominalWidth[i] < col.getWidth()) { percentSpace = (col.getWidth() - nominalWidth[i]) / spareSpace; shrinkFactor = redistribute * percentSpace; if (debug) { System.err.println("Column " + i + ": percentSpace = " + percentSpace + " , reducing by " + shrinkFactor + ", new width = " + (col.getWidth() - shrinkFactor)); } col.setPreferredWidth((int) (col.getWidth() - shrinkFactor)); } else // need to grow { // what percentage of the overage goes to this col? percentOver = (nominalWidth[i] - col.getWidth()) / totalOver; growthFactor = redistribute * percentOver; if (debug) { System.err.println("Column " + i + ": percentOver = " + percentOver + " , growing by " + growthFactor + ", new width = " + (col.getWidth() + growthFactor)); } col.setPreferredWidth((int) (col.getWidth() + growthFactor)); } } } } /** * <p>Regenerate the cell renderer for a column after it has been * size adjusted.</p> */ private void reflowColumn(TableColumn column) { if (column == null) { return; } if (myModel.getColumnClass(column.getModelIndex()) != Date.class) { column.setCellRenderer(new TextAreaRenderer()); } } /** * Print the JTable WYSIWYG in landscape mode */ public void print() { try { Printable printable = table.getPrintable(JTable.PrintMode.FIT_WIDTH, null, new MessageFormat(ts.l("print.page_template"))); // Page - {0} PrinterJob job = PrinterJob.getPrinterJob(); job.setPrintable(printable); PrintRequestAttributeSet attr = new HashPrintRequestAttributeSet(); if (job.printDialog(attr)) { job.print(attr); } } catch (PrinterException pe) { System.err.println("Error printing: " + pe.getMessage()); } } /** * Gets a key value from the row at rownum * * @param rownum integer value of the row number */ public Object getRowKey(int rownum) { return myModel.getRowHandler(rownum).key; } /** * Creates a new row, adds it to the hashtable * * @param key A hashtable key to be used to refer to this row in the future */ public void newRow(Object key) { myModel.newRow(key); } public void clearRows() { myModel.clearRows(); } public void refresh() { myModel.fireTableDataChanged(); } /** * Sets the contents of a cell in the table. * * @param key key to the row of the cell to be changed * @param col column of the cell to be changed * @param value A piece of data to be held with this cell, will be used for sorting */ public final void setCellValue(Object key, int col, Object value) { myModel.setCellValue(key, col, value); } /** * Gets the contents of a cell in the table. * * @param key key to the row of the cell * @param col column of the cell */ public final Object getCellValue(Object key, int col) { return myModel.getCellValue(key, col); } /** * pass in and set entire array of column names, or column headers */ public void setColumnNames(int columnCnt, String[] columns) { myModel.setColumnNames(columnCnt, columns); } /** * Erases all the cells in the table and removes any per-cell * attribute sets. */ public void clearCells() { index = new HashMap<Object, Integer>(); } /*---------------------------------------------------------------------------- inner class ColumnReflowListener ----------------------------------------------------------------------------*/ /** * <p>A TableColumnModelListener that takes care of reflowing columns * when they have been resized.</p> */ public class ColumnReflowListener implements TableColumnModelListener { public void columnAdded(TableColumnModelEvent e) { } public void columnMarginChanged(ChangeEvent e) { TableColumn tc = table.getTableHeader().getResizingColumn(); reflowColumn(tc); reflowColumn(getNextColumn(tc)); } /** * Get the TableModel index number for the visible column to the * right of colIndex. */ private TableColumn getNextColumn(TableColumn col) { if (col == null) { return null; } TableColumnModel colModel = table.getTableHeader().getColumnModel(); // get list of columns in physical order. Enumeration<TableColumn> columns = colModel.getColumns(); while (columns.hasMoreElements()) { if (columns.nextElement() == col) { if (columns.hasMoreElements()) { return columns.nextElement(); } } } return null; } public void columnMoved(TableColumnModelEvent e) { } public void columnRemoved(TableColumnModelEvent e) { return; } public void columnSelectionChanged(ListSelectionEvent e) { } } /*---------------------------------------------------------------------------- inner class SmartTableComponentListener ----------------------------------------------------------------------------*/ /** * listener for when the main panel is resized */ public class SmartTableComponentListener implements ComponentListener { public void componentResized(ComponentEvent e) { calcResizeMode(); } public void componentMoved(ComponentEvent e) { } public void componentShown(ComponentEvent e) { } public void componentHidden(ComponentEvent e) { } } /*---------------------------------------------------------------------------- inner class SmartTableAncestorListener ----------------------------------------------------------------------------*/ /** * <p>Class to assist with FixTable Columns, allowing it to be called * AFTER the table is drawn, to get in the correct Table and Panel * size to match with</p> */ class SmartTableAncestorListener implements AncestorListener { public void ancestorAdded(AncestorEvent e) { calcResizeMode(); } public void ancestorMoved(AncestorEvent e) { } public void ancestorRemoved(AncestorEvent e) { } } /*---------------------------------------------------------------------------- inner class Popuplistener ----------------------------------------------------------------------------*/ /** * Mouse class to control the right click menus */ class PopupListener extends MouseAdapter { SmartTable master_control; JPopupMenu popMenu; /* -- */ public PopupListener(SmartTable master, JPopupMenu popMenu) { this.master_control = master; this.popMenu = popMenu; } public void mousePressed(MouseEvent e) { // on some platforms, popups are triggered on mousedown showPopup(e); } public void mouseReleased(MouseEvent e) { // on others, on up showPopup(e); } private void showPopup(MouseEvent e) { if (!e.isPopupTrigger()) { return; } if (debug) { System.out.println("mouseevent on row:"+ table.rowAtPoint(e.getPoint()) +"*"); System.out.println("mouseevent on col:"+ table.columnAtPoint(e.getPoint()) +"*"); } master_control.remember_row = master_control.table.rowAtPoint(e.getPoint()); master_control.remember_col = master_control.remember_col2 = master_control.table.columnAtPoint(e.getPoint()); // if table is altered by moving or deleting a column, get true // column number here if (e.getSource() instanceof JMenuItem) { JTableHeader h = (JTableHeader) e.getSource(); TableColumnModel columnModel = h.getColumnModel(); int viewColumn = columnModel.getColumnIndexAtX(e.getX()); int column = columnModel.getColumn(viewColumn).getModelIndex(); if (debug) { System.out.println("second way is col:"+ column +"*"); } master_control.remember_col = column; } // Select a single row for right clicks - rows 1 to 1 table.addRowSelectionInterval(master_control.remember_row, master_control.remember_row); popMenu.show(e.getComponent(), e.getX(), e.getY()); } } /*---------------------------------------------------------------------------- inner class MyTableModel ----------------------------------------------------------------------------*/ /** * New Class added in to help define the table results. */ class MyTableModel extends AbstractTableModel { private final boolean DEBUG = true; /** * Vector of rowHandle objects, holds actual data cells */ public Vector<rowHandler> rows; private String[] columnNames; private Class[] columnClasses; /* -- */ public MyTableModel(String[] columnValues) { rows = new Vector<rowHandler>(); columnNames = columnValues; columnClasses = new Class[columnValues.length]; } public int getColumnCount() { return columnNames.length; } public int getRowCount() { return rows.size(); } public void newRow(Object key) { if (index.containsKey(key)) { throw new IllegalArgumentException("newRow(): row " + key + " already exists."); } rowHandler newRow1 = new rowHandler(key, getColumnCount()); rows.add(newRow1); index.put(key, Integer.valueOf(rows.size()-1)); } public void clearRows() { index = new HashMap<Object, Integer>(); // Clear Keys rows = new Vector<rowHandler>(); // Clear Rows } public String getColumnName(int col) { return columnNames[col]; } /** * pass in and set entire array of column names, or column headers */ public void setColumnNames(int columnCnt, String[] columns) { columnNames = columns; } public Object getValueAt(int row, int col) { return getRowHandler(row).cells[col]; } public void setValueAt(Object value, int row, int col) { if (value != null && columnClasses[col] == null) { columnClasses[col] = value.getClass(); } getRowHandler(row).cells[col] = value; } /** * Sets the contents of a cell in the table. * * @param key key to the row of the cell to be changed * @param col column of the cell to be changed * @param value A piece of data to be held with this cell, will be used for sorting */ public final void setCellValue(Object key, int col, Object value) { Integer row = index.get(key); int row2 = row.intValue(); setValueAt(value, row2, col); } /** * Gets the contents of a cell in the table. * * @param key key to the row of the cell * @param col column of the cell */ public final Object getCellValue(Object key, int col) { Integer row = index.get(key); int row2 = row.intValue(); return getValueAt(row2, col); } public rowHandler getRowHandler(int row) { return rows.get(row); } /** * JTable uses this method to determine the default renderer/ * editor for each cell. */ public Class getColumnClass(int c) { if (columnClasses[c] == null) { return Object.class; } else { return columnClasses[c]; } } /** * Don't need to implement this method unless your table's * editable. */ public boolean isCellEditable(int row, int col) { return false; // For our tables, all columns are uneditable // Note that the data/cell address is constant, // no matter where the cell appears onscreen. } private void printDebugData() { int numRows = getRowCount(); int numCols = getColumnCount(); System.out.println("numCols = " + numCols); for (int i=0; i < numRows; i++) { System.out.print(" row " + i + ":"); for (int j=0; j < numCols; j++) { System.out.print(" " + getValueAt(i,j)); } System.out.println(); } System.out.println("--------------------------"); } } /*---------------------------------------------------------------------------- nested class DateCellRenderer ----------------------------------------------------------------------------*/ /** * A cell renderer for Date values. */ private static class DateCellRenderer extends TextAreaRenderer { /** * Cached FontMetrics object, used to calculate the necessary width * for a specific string in the SmartTable's optimizeCols() * method. */ private FontMetrics metrics = null; /* -- */ /** * Returns the component that is used for rendering the value. * * @param table the JTable * @param value the value of the object * @param isSelected is the cell selected? * @param hasFocus has the cell the focus? * @param row the row to render * @param column the cell to render * * @return this component (the default table cell renderer) */ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (value instanceof Date) { Date dateValue = (Date) value; setText(getString(dateValue)); } return this; } /** * Returns the necessary width required to render value with this * renderer, given the table's defined font. */ public int getUnwrappedWidth(JTable table, Object value) { if (value == null) { return 0; } if (metrics == null) { metrics = this.getFontMetrics(table.getFont()); } Date dateValue = (Date) value; return metrics.stringWidth(getString(dateValue)); } private String getString(Date dateValue) { return new SimpleDateFormat(SmartTable.datePattern).format(dateValue); } } } /*------------------------------------------------------------------------------ class rowHandler ------------------------------------------------------------------------------*/ /** * This class is used to map a hash key to a position in the table. */ class rowHandler { Object key; Object[] cells; public rowHandler(Object key, int columns) { cells = new Object[columns]; this.key = key; } } /*------------------------------------------------------------------------------ class MyJTableHeader ------------------------------------------------------------------------------*/ /** * Interceptor class to prevent right-clicks from dragging columns in * our SmartTable. */ class MyJTableHeader extends JTableHeader { public MyJTableHeader(TableColumnModel model) { super(model); } protected void processMouseEvent(MouseEvent e) { setReorderingAllowed(!e.isPopupTrigger()); super.processMouseEvent(e); } }