/* rowTable.java A JDK 1.1 table Swing component. Created: 14 June 1996 Module By: Jonathan Abbey, jonabbey@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.Color; import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Date; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.Hashtable; import java.util.List; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import arlut.csd.Util.TranslationService; /*------------------------------------------------------------------------------ class rowTable ------------------------------------------------------------------------------*/ /** * <p>rowTable is a specialized baseTable, supporting a per-row * access model based on a hashtable.</p> */ public class rowTable extends baseTable 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.rowTable"); static final public String menuTitle = ts.l("global.menu_title"); // "Column Menu" static final public String sortByStr = ts.l("global.sort_by"); // "Sort By This Column" static final public String revSortByStr = ts.l("global.rev_sort_by"); // "Reverse Sort By This Column" static final public String delColStr = ts.l("global.del_col"); // "Delete This Column" static final public String optColWidStr = ts.l("global.opt_col_widths"); // "Optimize Column Widths" Hashtable<Object, rowHandle> index; Vector<rowHandle> crossref; rowSelectCallback callback; JPopupMenu rowMenu; JMenuItem SortByMI; JMenuItem RevSortByMI; JMenuItem DeleteColMI; JMenuItem OptimizeMI; // we remember the last two sorts that have been performed so that // we can be requested to automatically re-do the primary and // secondary stable sorts after the data is changed by the user of // rowTable. private int lastSortColumn = -1; private boolean lastSortForward = true; private int olderSortColumn = -1; private boolean olderSortForward = true; /** * <p>The hash key for the selected row, or null if no row is * selected.</p> */ Object rowSelectedKey; /* -- */ /** * This is the base constructor for rowTable, which allows * all aspects of the rowTable's appearance and behavior * to be customized. * * @param headerAttrib attribute set for the column headers * @param tableAttrib default attribute set for the body of the table * @param colAttribs per column attribute sets * @param colWidths array of initial column widths * @param vHeadLineColor color of vertical lines in the column headers, if any * @param vRowLineColor color of vertical lines in the table body, if any * @param hHeadLineColor color of horizontal lines in the column headers, if any * @param hRowLineColor color of vertical lines in the table body, if any * @param headers array of column header titles, must be same size as colWidths * @param horizLines true if horizontal lines should be shown between rows in report table * @param vertLines true if vertical lines should be shown between columns in report table * @param vertFill true if table should expand vertically to fill size of baseTable * @param hVertFill true if horizontal lines should be drawn in the vertical fill region * (only applies if vertFill and horizLines are true) * @param callback reference to an object that implements the rowSelectCallback interface * @param menu reference to a popup menu to be associated with rows in this table * @param allowDeleteColumn if true, a 'Delete This Column' menu item will be added * to the per-column popup menu * */ public rowTable(tableAttr headerAttrib, tableAttr tableAttrib, tableAttr[] colAttribs, int[] colWidths, Color vHeadLineColor, Color vRowLineColor, Color hHeadLineColor, Color hRowLineColor, String[] headers, boolean horizLines, boolean vertLines, boolean vertFill, boolean hVertFill, rowSelectCallback callback, JPopupMenu menu, boolean allowDeleteColumn) { super(headerAttrib, tableAttrib, colAttribs, colWidths, vHeadLineColor, vRowLineColor, hHeadLineColor, hRowLineColor, headers, horizLines, vertLines, vertFill, hVertFill, menu, null); rowMenu = new JPopupMenu(); // rowMenu.add(new JLabel(menuTitle)); // rowMenu.addSeparator(); if (colWidths.length > 1) { SortByMI = new JMenuItem(sortByStr); SortByMI.setActionCommand(sortByStr); RevSortByMI = new JMenuItem(revSortByStr); RevSortByMI.setActionCommand(revSortByStr); DeleteColMI = new JMenuItem(delColStr); DeleteColMI.setActionCommand(delColStr); OptimizeMI = new JMenuItem(optColWidStr); OptimizeMI.setActionCommand(optColWidStr); } else { SortByMI = new JMenuItem(sortByStr); SortByMI.setActionCommand(sortByStr); RevSortByMI = new JMenuItem(revSortByStr); RevSortByMI.setActionCommand(revSortByStr); } rowMenu.add(SortByMI); rowMenu.add(RevSortByMI); if (colWidths.length > 1) { if (allowDeleteColumn) { rowMenu.add(DeleteColMI); } rowMenu.add(OptimizeMI); } SortByMI.addActionListener(this); RevSortByMI.addActionListener(this); if (colWidths.length > 1) { if (allowDeleteColumn) { DeleteColMI.addActionListener(this); } OptimizeMI.addActionListener(this); } canvas.add(rowMenu); this.headerMenu = rowMenu; this.callback = callback; index = new Hashtable<Object, rowHandle>(); crossref = new Vector<rowHandle>(); } /** * Constructor with default fonts, justification, and behavior * * @param colWidths array of initial column widths * @param headers array of column header titles, must be same size as colWidths * @param callback reference to an object that implements the rowSelectCallback interface * @param horizLines draw horizontal lines between rows? * @param menu reference to a popup menu to be associated with rows in this table * */ public rowTable(int[] colWidths, String[] headers, rowSelectCallback callback, boolean horizLines, JPopupMenu menu, boolean allowDeleteColumn) { this(new tableAttr(null, new Font("SansSerif", Font.BOLD, 14), Color.white, Color.blue, tableAttr.JUST_CENTER), new tableAttr(null, new Font("SansSerif", Font.PLAIN, 12), Color.black, Color.white, tableAttr.JUST_LEFT), (tableAttr[]) null, colWidths, Color.black, Color.black, Color.black, Color.black, headers, horizLines, true, true, false, callback, menu, allowDeleteColumn); // we couldn't pass this to the baseTableConstructors // above, so we set it directly here, then force metrics // calculation headerAttrib.c = this; headerAttrib.calculateMetrics(); tableAttrib.c = this; tableAttrib.calculateMetrics(); calcFonts(); } /** * Constructor with default fonts, justification, and behavior * * @param colWidths array of initial column widths * @param headers array of column header titles, must be same size as colWidths * @param callback reference to an object that implements the rowSelectCallback interface * @param menu reference to a popup menu to be associated with rows in this table * */ public rowTable(int[] colWidths, String[] headers, rowSelectCallback callback, JPopupMenu menu, boolean allowDeleteColumn) { this(new tableAttr(null, new Font("SansSerif", Font.BOLD, 14), Color.white, Color.blue, tableAttr.JUST_CENTER), new tableAttr(null, new Font("SansSerif", Font.PLAIN, 12), Color.black, Color.white, tableAttr.JUST_LEFT), (tableAttr[]) null, colWidths, Color.black, Color.black, Color.black, Color.black, headers, true, true, true, false, callback, menu, allowDeleteColumn); // we couldn't pass this to the baseTableConstructors // above, so we set it directly here, then force metrics // calculation headerAttrib.c = this; headerAttrib.calculateMetrics(); tableAttrib.c = this; tableAttrib.calculateMetrics(); calcFonts(); } /** * Hook for subclasses to implement selection logic * * @param x col of cell clicked in * @param y row of cell clicked in */ public synchronized void clickInCell(int x, int y, boolean rightButton) { rowHandle element = null; /* -- */ // find the key for the selected row if (debug) { System.err.println("rowTable.clickInCell(" + x + "," + y + "): seeking key"); } element = findRow(y); if (debug) { if (element == null) { System.err.println("rowTable.clickInCell(" + x + "," + y + "): key not found"); } else { System.err.println("rowTable.clickInCell(" + x + "," + y + "): found key " + element.key); } } if (!element.key.equals(rowSelectedKey)) { if (debug) { System.err.println("rowTable.clickInCell(" + x + "," + y + "): clicked in unselected row.. unselecting"); } unSelectRow(); if (debug) { System.err.println("rowTable.clickInCell(" + x + "," + y + "): clicked in unselected row.. selecting"); } selectRow(element.key, false); if (debug) { System.err.println("rowTable.clickInCell(" + x + "," + y + "): clicked in unselected row.. refreshing"); } refreshTable(); if (debug) { System.err.println("rowTable.clickInCell(" + x + "," + y + "): table refreshed"); } } else { // go ahead and deselect the current row, if and only if this is a left-button // click. if (!rightButton) { if (debug) { System.err.println("rowTable.clickInCell(" + x + "," + y + "): clicked in selected row.. unselecting"); } unSelectRow(); if (debug) { System.err.println("rowTable.clickInCell(" + x + "," + y + "): clicked in selected row.. refreshing"); } refreshTable(); if (debug) { System.err.println("rowTable.clickInCell(" + x + "," + y + "): table refreshed"); } } } } /** * Hook for subclasses to implement selection logic * * @param x col of cell double clicked in * @param y row of cell double clicked in */ public synchronized void doubleClickInCell(int x, int y) { rowHandle element = null; /* -- */ element = findRow(y); if (element.key.equals(rowSelectedKey)) { callback.rowDoubleSelected(element.key); } else { // the first click of our double click deselected // the row, go ahead and reselect it clickInCell(x,y); } } /** * * Unselect all cells.. override of a baseTable method. * */ public void unSelectAll() { unSelectRow(); refreshTable(); } public Object getSelectedRow() { return rowSelectedKey; } /** * * This method unselects any rows currently selected that do not * match key and selects the row that does match key (if any). * * @param key The key to the row to be selected * */ public synchronized void selectRow(Object key, boolean refreshTable) { // unselect the currently selected row, if any. Note that we // are currently only supporting single row selection. unSelectRow(); if (key != null) { rowHandle row = index.get(key); if (row == null) { return; } selectRow(row.rownum); rowSelectedKey = key; } if (refreshTable) { refreshTable(); } if (callback != null) { callback.rowSelected(key); } } /** * * This method unselects the currently selected row. * */ public synchronized void unSelectRow() { if (rowSelectedKey != null) { rowHandle row = index.get(rowSelectedKey); if (row == null) { return; } unSelectRow(row.rownum); if (callback != null) { callback.rowSelected(rowSelectedKey); } rowSelectedKey = null; } } /** * * Erases all the cells in the table and removes any per-cell * attribute sets. * */ public void clearCells() { index = new Hashtable<Object, rowHandle>(); crossref = new Vector<rowHandle>(); super.clearCells(); rowSelectedKey = null; } /** * 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) { rowHandle element; /* -- */ if (index.containsKey(key)) { throw new IllegalArgumentException("rowTable.newRow(): row " + key + " already exists."); } element = new rowHandle(this, key); index.put(key, element); } /** * Deletes a row. * * @param key A hashtable key for the row to delete * @param repaint true if the table should be redrawn after the row is deleted */ public void deleteRow(Object key, boolean repaint) { rowHandle element; /* -- */ if (!index.containsKey(key)) { // no such row exists.. what to do? return; } if (key.equals(rowSelectedKey)) { unSelectRow(); } element = index.get(key); index.remove(key); // delete the row from our parent.. super.deleteRow(element.rownum, repaint); // sync up the rowHandles crossref.remove(element.rownum); // and make sure the rownums are correct. for (int i = element.rownum; i < crossref.size(); i++) { crossref.get(i).rownum = i; } reShape(); } /** * Gets a cell based on hashkey * * @param key A hashtable key for the row of the cell * @param col Column number, range 0..# of columns - 1 * */ // ? can this be done? will java do the right thing // for method overloading? public tableCell getCell(Object key, int col) { return super.getCell(col, index.get(key).rownum); } // -------------------- convenience methods -------------------- /** * 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 cellText the text to place into cell * @param repaint true if the table should be redrawn after changing cell * */ public final void setCellText(Object key, int col, String cellText, boolean repaint) { setCellText(getCell(key, col), cellText, repaint); } /** * 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 cellText the text to place into cell * @param data A piece of data to be held with this cell, will be used for sorting * @param repaint true if the table should be redrawn after changing cell * */ public final void setCellText(Object key, int col, String cellText, Object data, boolean repaint) { tableCell cell; cell = getCell(key, col); cell.setData(data); setCellText(cell, cellText, repaint); } /** * 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 String getCellText(Object key, int col) { return getCellText(getCell(key,col)); } /** * Sets the tableAttr 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 attr the tableAttr to assign to cell * @param repaint true if the table should be redrawn after changing cell * */ public final void setCellAttr(Object key, int col, tableAttr attr, boolean repaint) { setCellAttr(getCell(key,col), attr, repaint); } /** * Gets the tableAttr of a cell in the table. * * @param key key to the row of the cell * @param col column of the cell */ public final tableAttr getCellAttr(Object key, int col) { return getCellAttr(getCell(key,col)); } /** * Sets the font of a cell in the table. * * A font of (Font) null will cause baseTable to revert to using the * table or column's default font for this cell. * * @param key key to the row of the cell * @param col column of the cell * @param font the Font to assign to cell, may be null to use default * @param repaint true if the table should be redrawn after changing cell * */ public final void setCellFont(Object key, int col, Font font, boolean repaint) { setCellFont(getCell(key,col), font, repaint); } /** * Sets the justification of a cell in the table. * * Use tableAttr.JUST_INHERIT to have this cell use default justification * * @param key key to the row of the cell * @param col column of the cell * @param just the justification to assign to cell * @param repaint true if the table should be redrawn after changing cell * * @see tableAttr */ public final void setCellJust(Object key, int col, int just, boolean repaint) { setCellJust(getCell(key,col),just,repaint); } /** * Sets the foreground color of a cell * * A color of (Color) null will cause baseTable to revert to using the * foreground selected for the column (if defined) or the foreground for * the table. * * @param key key to the row of the cell * @param col column of the cell * @param color the Color to assign to cell * @param repaint true if the table should be redrawn after changing cell * */ public final void setCellColor(Object key, int col, Color color, boolean repaint) { setCellColor(getCell(key,col),color,repaint); } /** * Sets the background color of a cell * * A color of (Color) null will cause baseTable to revert to using the * background selected for the column (if defined) or the background for * the table. * * @param key key to the row of the cell * @param col column of the cell * @param color the Color to assign to cell * @param repaint true if the table should be redrawn after changing cell * */ public final void setCellBackColor(Object key, int col, Color color, boolean repaint) { setCellBackColor(getCell(key,col),color,repaint); } /** * Returns true if a key is already in use in the table * * @param key key to look for in the table */ public boolean containsKey(Object key) { return index.containsKey(key); } /** * Return an enumeration of the keys in the table * */ public Enumeration keys() { return index.keys(); } /** * Method used to handle the popup menu */ public void actionPerformed(ActionEvent e) { rowHandle element = null; /* -- */ // System.err.println("rowTable.actionPerformed"); if (callback == null) { return; } try { if (menuRow == -1) { if (e.getSource() == DeleteColMI) { callback.colMenuPerformed(menuCol, e); this.deleteColumn(menuCol, true); refreshTable(); } else if (e.getSource() == SortByMI) { callback.colMenuPerformed(menuCol, e); resort(menuCol, true, true); } else if (e.getSource() == RevSortByMI) { callback.colMenuPerformed(menuCol, e); resort(menuCol, false, true); } else if (e.getSource() == OptimizeMI) { callback.colMenuPerformed(menuCol, e); optimizeCols(); refreshTable(); } return; } element = findRow(menuRow); // perform our callback if (element != null) { callback.rowMenuPerformed(element.key, e); } } finally { // clear our lastpopped menu row, col menuRow = -1; menuCol = -1; } } /** * <p>Sets the sort preferences for this rowTable from a String * encoding suitable for storage in a Java Prefrences object.</p> */ public void setSortPref(String sortPref) { lastSortColumn = -1; olderSortColumn = -1; if (sortPref == null || sortPref.equals("")) { return; } String regex = "(\\d+)([fr])(:(\\d+)([fr]))?"; Pattern pat = Pattern.compile(regex); Matcher mat = pat.matcher(sortPref); if (!mat.matches()) { return; } String lastCol = mat.group(1); String lastOrder = mat.group(2); String olderCol = mat.group(4); String olderOrder = mat.group(5); lastSortColumn = Integer.parseInt(lastCol); lastSortForward = lastOrder.equals("f"); if (olderCol != null) { olderSortColumn = Integer.parseInt(olderCol); olderSortForward = olderOrder.equals("f"); } } /** * <p>Gets the sort preferences for this rowTable as a String * encoding suitable for storage in a Java Prefrences object.</p> */ public String getSortPref() { StringBuilder builder = new StringBuilder(); if (lastSortColumn == -1) { return ""; } builder.append(lastSortColumn); if (lastSortForward) { builder.append("f"); } else { builder.append("r"); } if (olderSortColumn != -1) { builder.append(":"); builder.append(olderSortColumn); if (olderSortForward) { builder.append("f"); } else { builder.append("r"); } } return builder.toString(); } /** * <p>Sort by the last two sort columns and sort orders, if set.</p> */ public void resort(boolean repaint) { if (lastSortColumn == -1) { return; } if (olderSortColumn != -1) { new rowSorter(this, olderSortColumn, olderSortForward).sort(); } new rowSorter(this, lastSortColumn, lastSortForward).sort(); if (repaint) { refreshTable(); } } /** * <p>Do a sort by column and forward direction.</p> */ public void resort(int column, boolean forward, boolean repaint) { if (column == this.lastSortColumn) { this.olderSortColumn = -1; } else { this.olderSortColumn = this.lastSortColumn; this.olderSortForward = this.lastSortForward; } this.lastSortColumn = column; this.lastSortForward = forward; new rowSorter(this, column, forward).sort(); if (repaint) { refreshTable(); } } private rowHandle findRow(int y) { for (rowHandle row: crossref) { if (row.rownum == y) { return row; } } throw new RuntimeException("Couldn't find row " + y); } } /*------------------------------------------------------------------------------ class rowHandle ------------------------------------------------------------------------------*/ /** * <p>This class is used to map a hash key to a row in the table.</p> */ class rowHandle { Object key; int rownum; tableRow element; public rowHandle(rowTable parent, Object key) { parent.addRow(false); // don't repaint table rownum = parent.rows.size() - 1; this.key = key; // crossref's index for RowHash element should be same as // rows's index for the corresponding ReportRow parent.crossref.add(this); // check to make sure if (parent.crossref.indexOf(this) != rownum) { throw new RuntimeException("rowTable / baseTable mismatch"); } } } /*------------------------------------------------------------------------------ class rowSorter ------------------------------------------------------------------------------*/ class rowSorter implements Comparator<rowHandle> { rowTable parent; List<rowHandle> rows; boolean forward; int column; /* -- */ public rowSorter(rowTable parent, int column, boolean forward) { this.parent = parent; this.column = column; this.forward = forward; } public int compare(rowHandle a, rowHandle b) { Object Adata, Bdata; try { Adata = a.element.get(column).getData(); } catch (NullPointerException ex) { Adata = null; } try { Bdata = b.element.get(column).getData(); } catch (NullPointerException ex) { Bdata = null; } // Adata and/or Bdata will be null if we are just comparing // strings, rather than the attached integer or date values for // numeric or temporal sorting. If one but not both of Adata and // Bdata are null, the text of the column will hopefully be // matching null, and we'll do the right thing by always treating // the null string as lesser in our sort if (Adata == null || Bdata == null) { String one, two; if (forward) { try { one = a.element.get(column).text; } catch (NullPointerException ex) { one = null; } try { two = b.element.get(column).text; } catch (NullPointerException ex) { two = null; } } else { try { one = b.element.get(column).text; } catch (NullPointerException ex) { one = null; } try { two = a.element.get(column).text; } catch (NullPointerException ex) { two = null; } } // null is always lesser if (one == null) { if (two == null) { return 0; } else { return -1; } } else if (two == null) { return 1; } // okay, neither null. return one.compareToIgnoreCase(two); } // if we are sorting dates, we expect everything in this column // to be a date if (Adata instanceof Date) { Date Adate = (Date) Adata; Date Bdate = (Date) Bdata; if (forward) { if (Adate.before(Bdate)) { return -1; } else if (Bdate.before(Adate)) { return 1; } else { return 0; } } else { if (Bdate.before(Adate)) { return -1; } else if (Adate.before(Bdate)) { return 1; } else { return 0; } } } if (Adata instanceof Integer) { int ia = ((Integer) Adata).intValue(); int ib = ((Integer) Bdata).intValue(); if (forward) { if (ia < ib) { return -1; } else if (ia > ib) { return 1; } else { return 0; } } else { if (ib < ia) { return -1; } else if (ib > ia) { return 1; } else { return 0; } } } if (Adata instanceof Double) { double da = ((Double) Adata).doubleValue(); double db = ((Double) Bdata).doubleValue(); if (forward) { if (da < db) { return -1; } else if (da > db) { return 1; } else { return 0; } } else { if (db < da) { return -1; } else if (db > da) { return 1; } else { return 0; } } } // unrecognized data type.. can't compare return 0; } public void sort() { if (parent.rows.size() < 2) { return; } rows = new ArrayList<rowHandle>(); int i = 0; for (tableRow tRow: parent.rows) { rowHandle row = parent.crossref.get(i); row.element = tRow; rows.add(row); i++; } // NB: Collections.sort() is stable, so we won't affect the // pre-existing ordering for rows with identical keys in the sort // column Collections.sort(rows, this); i = 0; for (rowHandle row: rows) { row.rownum = i; parent.crossref.set(i, row); parent.rows.set(i, row.element); i++; } parent.reCalcRowPos(0); // recalc vertical positions } }