/* * RapidMiner * * Copyright (C) 2001-2011 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.tools; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.Arrays; import java.util.Date; import javax.swing.Action; import javax.swing.JMenu; import javax.swing.JPopupMenu; import javax.swing.JTable; import javax.swing.JViewport; import javax.swing.ListSelectionModel; import javax.swing.event.ChangeEvent; import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.actions.MoveColumnAction; import com.rapidminer.gui.tools.actions.AddToSortingColumnsAction; import com.rapidminer.gui.tools.actions.EqualColumnWidthsAction; import com.rapidminer.gui.tools.actions.FitAllColumnWidthsAction; import com.rapidminer.gui.tools.actions.FitColumnWidthAction; import com.rapidminer.gui.tools.actions.RestoreOriginalColumnOrderAction; import com.rapidminer.gui.tools.actions.SelectColumnAction; import com.rapidminer.gui.tools.actions.SelectRowAction; import com.rapidminer.gui.tools.actions.SortByColumnAction; import com.rapidminer.gui.tools.actions.SortColumnsAccordingToNameAction; import com.rapidminer.gui.tools.components.ToolTipWindow; import com.rapidminer.gui.tools.components.ToolTipWindow.TipProvider; import com.rapidminer.report.Tableable; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.Tools; import com.rapidminer.tools.container.Pair; /** * <p>This class extends a JTable in a way that editing is handled like it is expected, i.e. * editing is properly stopped during focus losts, resizing, or column movement. The current * value is then set to the model. The only way to abort the value change is by pressing * the escape key.</p> * * <p>The extended table is sortable per default. Developers should note that this feature * might lead to problems if the columns contain different class types and different editors. * In this case one of the constructors should be used which set the sortable flag to false. * </p> * * @author Ingo Mierswa */ public class ExtendedJTable extends JTable implements Tableable, MouseListener { private static final long serialVersionUID = 4840252601155251257L; private static final int DEFAULT_MAX_ROWS_FOR_SORTING = 100000; private static final int DEFAULT_COLUMN_WIDTH = 100; public static final int NO_DATE_FORMAT = -1; public static final int DATE_FORMAT = 0; public static final int TIME_FORMAT = 1; public static final int DATE_TIME_FORMAT = 2; private Action ROW_ACTION = new SelectRowAction(this, IconSize.SMALL); private Action COLUMN_ACTION = new SelectColumnAction(this, IconSize.SMALL); private Action FIT_COLUMN_ACTION = new FitColumnWidthAction(this, IconSize.SMALL); private Action FIT_ALL_COLUMNS_ACTION = new FitAllColumnWidthsAction(this, IconSize.SMALL); private Action EQUAL_WIDTHS_ACTION = new EqualColumnWidthsAction(this, IconSize.SMALL); private Action SORTING_DESCENDING_ACTION = new SortByColumnAction(this, ExtendedJTableSorterModel.DESCENDING, IconSize.SMALL); private Action SORTING_ASCENDING_ACTION = new SortByColumnAction(this, ExtendedJTableSorterModel.ASCENDING, IconSize.SMALL); private Action ADD_TO_SORTING_DESCENDING_ACTION = new AddToSortingColumnsAction(this, ExtendedJTableSorterModel.DESCENDING, IconSize.SMALL); private Action ADD_TO_SORTING_ASCENDING_ACTION = new AddToSortingColumnsAction(this, ExtendedJTableSorterModel.ASCENDING, IconSize.SMALL); private Action SORT_COLUMNS_BY_NAME_ACTION = new SortColumnsAccordingToNameAction(this, IconSize.SMALL); private Action RESTORE_COLUMN_ORDER_ACTION = new RestoreOriginalColumnOrderAction(this, IconSize.SMALL); private boolean sortable = true; private CellColorProvider cellColorProvider = new CellColorProviderAlternating(); private boolean useColoredCellRenderer = true; private transient ColoredTableCellRenderer renderer = new ColoredTableCellRenderer(); private ExtendedJTableSorterModel tableSorter = null; private ExtendedJScrollPane scrollPaneParent = null; private ExtendedJTablePacker packer = null; private boolean fixFirstColumn = false; private String[] originalOrder = null; private boolean showPopopUpMenu = true; private boolean[] cutOnLineBreaks; private int[] maximalTextLengths; public ExtendedJTable() { this(null, true); } public ExtendedJTable(boolean sortable) { this(null, sortable); } public ExtendedJTable(TableModel model, boolean sortable) { this(model, sortable, true); } public ExtendedJTable(TableModel model, boolean sortable, boolean columnMovable) { this(model, sortable, columnMovable, true); } public ExtendedJTable(boolean sortable, boolean columnMovable, boolean autoResize) { this(null, sortable, columnMovable, autoResize); } public ExtendedJTable(TableModel model, boolean sortable, boolean columnMovable, boolean autoResize) { this(model, sortable, columnMovable, autoResize, true, false); } public ExtendedJTable(TableModel model, boolean sortable, boolean columnMovable, boolean autoResize, boolean useColoredCellRenderer, boolean fixFirstColumn) { super(); this.sortable = sortable; this.useColoredCellRenderer = useColoredCellRenderer; this.fixFirstColumn = fixFirstColumn; // allow all kinds of selection (e.g. for copy and paste) setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); setColumnSelectionAllowed(true); setRowSelectionAllowed(true); setRowHeight(getRowHeight() + SwingTools.TABLE_ROW_EXTRA_HEIGHT); getTableHeader().setReorderingAllowed(columnMovable); // necessary in order to fix changes after focus was lost putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); // auto resize? if (!autoResize) setAutoResizeMode(AUTO_RESIZE_OFF); if (model != null) { setModel(model); } // add listener for automatically resizing the table for double clicking the header border getTableHeader().addMouseListener(new ExtendedJTableColumnFitMouseListener()); addMouseListener(this); } /** Registers a new {@link ToolTipWindow} on this table. */ public void installToolTip() { // adding a new extended tool tip window new ToolTipWindow(new TableToolTipProvider(), this); setToolTipText(null); } protected Object readResolve() { this.renderer = new ColoredTableCellRenderer(); return this; } protected ExtendedJTableSorterModel getTableSorter() { return this.tableSorter; } /** Subclasses might overwrite this method which by default simply returns NO_DATE. * The returned format should be one out of NO_DATE_FORMAT, DATE_FORMAT, TIME_FORMAT, * or DATE_TIME_FORMAT. This information will be used for the cell renderer. */ public int getDateFormat(int row, int column) { return NO_DATE_FORMAT; } /** The given color provider will be used for the cell renderer. * The default method implementation returns {@link SwingTools#LIGHTEST_BLUE} and white for * alternating rows. If no colors should be used at all, set the cell color provider to * null or to the default white color provider {@link CellColorProviderWhite}. */ public void setCellColorProvider(CellColorProvider cellColorProvider) { this.cellColorProvider = cellColorProvider; } /** The returned color provider will be used for the cell renderer. * The default method implementation returns {@link SwingTools#LIGHTEST_BLUE} and white for * alternating rows. If no colors should be used at all, set the cell color provider to * null or to the default white color provider {@link CellColorProviderWhite}. */ public CellColorProvider getCellColorProvider() { return this.cellColorProvider; } public void setSortable(boolean sortable) { this.sortable = sortable; } public boolean isSortable() { return sortable; } public void setShowPopupMenu(boolean showPopupMenu) { this.showPopopUpMenu = showPopupMenu; } public void setFixFirstColumnForRearranging(boolean fixFirstColumn) { this.fixFirstColumn = fixFirstColumn; } public void setMaximalTextLength(int maximalTextLength) { Arrays.fill(maximalTextLengths, maximalTextLength); } public void setMaximalTextLength(int maximalTextLength, int column) { maximalTextLengths[column] = maximalTextLength; } public void setCutOnLineBreak(boolean enable) { Arrays.fill(cutOnLineBreaks, enable); } public void setCutOnLineBreak(boolean enable, int column) { cutOnLineBreaks[column] = enable; } @Override public void setModel(final TableModel model) { boolean shouldSort = this.sortable && checkIfSortable(model); if (shouldSort) { this.tableSorter = new ExtendedJTableSorterModel(model); this.tableSorter.setTableHeader(getTableHeader()); super.setModel(this.tableSorter); } else { super.setModel(model); this.tableSorter = null; } originalOrder = new String[model.getColumnCount()]; for (int c = 0; c < model.getColumnCount(); c++) { originalOrder[c] = model.getColumnName(c); } // initializing arrays for cell renderer settings cutOnLineBreaks = new boolean[model.getColumnCount()]; maximalTextLengths = new int[model.getColumnCount()]; Arrays.fill(maximalTextLengths, Integer.MAX_VALUE); model.addTableModelListener(new TableModelListener() { @Override public void tableChanged(TableModelEvent e) { int oldLength = cutOnLineBreaks.length; if (oldLength != model.getColumnCount()) { cutOnLineBreaks = Arrays.copyOf(cutOnLineBreaks, model.getColumnCount()); maximalTextLengths = Arrays.copyOf(maximalTextLengths, model.getColumnCount()); if (oldLength < cutOnLineBreaks.length) { Arrays.fill(cutOnLineBreaks, oldLength, cutOnLineBreaks.length, false); Arrays.fill(maximalTextLengths, oldLength, cutOnLineBreaks.length, Integer.MAX_VALUE); } } } }); } public void setSortingStatus(int status, boolean cancelSorting) { if (getModel() instanceof ExtendedJTableSorterModel) { ExtendedJTableSorterModel sorterModel = (ExtendedJTableSorterModel)getModel(); JTableHeader h = getTableHeader(); TableColumnModel columnModel = h.getColumnModel(); int viewColumn = getSelectedColumn(); if (viewColumn != -1) { int column = columnModel.getColumn(viewColumn).getModelIndex(); if (column != -1) { if (sorterModel.isSorting()) { if (cancelSorting) { sorterModel.cancelSorting(); } } sorterModel.setSortingStatus(column, status); } } } } public void pack() { packer = new ExtendedJTablePacker(true); if (isShowing()) { packer.pack(this); packer = null; } } @Override public void addNotify(){ super.addNotify(); if (packer != null){ packer.pack(this); packer = null; } } public void unpack() { JTableHeader header = getTableHeader(); if (header != null) { for (int c = 0; c < getColumnCount(); c++) { TableColumn tableColumn = header.getColumnModel().getColumn(c); header.setResizingColumn(tableColumn); // this line is very important int width = DEFAULT_COLUMN_WIDTH; if (getWidth() / width > getColumnCount()) { width = getWidth() / getColumnCount(); } tableColumn.setWidth(width); } } } public void packColumn() { JTableHeader header = getTableHeader(); if (header != null) { int col = getSelectedColumn(); if (col >= 0) { TableColumn tableColumn = header.getColumnModel().getColumn(col); if (tableColumn != null) { int width = (int)header.getDefaultRenderer().getTableCellRendererComponent(this, tableColumn.getIdentifier(), false, false, -1, col).getPreferredSize().getWidth(); int firstRow = 0; int lastRow = getRowCount(); ExtendedJScrollPane scrollPane = getExtendedScrollPane(); if (scrollPane != null) { JViewport viewport = scrollPane.getViewport(); Rectangle viewRect = viewport.getViewRect(); if (viewport.getHeight() < getHeight()) { firstRow = rowAtPoint(new Point(0, viewRect.y)); firstRow = Math.max(0, firstRow); lastRow = rowAtPoint(new Point(0, viewRect.y + viewRect.height - 1)); lastRow = Math.min(lastRow, getRowCount()); } } for (int row = firstRow; row < lastRow; row++){ int preferedWidth = (int)getCellRenderer(row, col).getTableCellRendererComponent(this, getValueAt(row, col), false, false, row, col).getPreferredSize().getWidth(); width = Math.max(width, preferedWidth); } header.setResizingColumn(tableColumn); // this line is very important tableColumn.setWidth(width + getIntercellSpacing().width); } } } } public void sortColumnsAccordingToNames() { int offset = 0; if (fixFirstColumn) { offset = 1; } for (int i = offset; i < getColumnCount(); i++) { int minIndex = -1; String minName = null; for (int j = i; j < getColumnCount(); j++) { String currentName = getColumnName(j); if (minName == null || currentName.compareTo(minName) < 0) { minName = currentName; minIndex = j; } } moveColumn(minIndex, i); } } public void restoreOriginalColumnOrder() { for (int i = 0; i < originalOrder.length; i++) { String nextColumn = originalOrder[i]; for (int j = i; j < getColumnCount(); j++) { String candidateName = getColumnName(j); if (nextColumn.equals(candidateName)) { moveColumn(j, i); break; } } } } @Override public Dimension getIntercellSpacing() { Dimension dimension = super.getIntercellSpacing(); dimension.width = dimension.width + 6; return dimension; } private boolean checkIfSortable(TableModel model) { int maxSortableRows = DEFAULT_MAX_ROWS_FOR_SORTING; String maxString = ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_RAPIDMINER_GUI_MAX_SORTABLE_ROWS); if (maxString != null) { try { maxSortableRows = Integer.parseInt(maxString); } catch (NumberFormatException e) { // do nothing } } if (model.getRowCount() > maxSortableRows) { return false; } else { return true; } } /** Necessary to properly stopping the editing when a column is moved (dragged). */ @Override public void columnMoved(TableColumnModelEvent e) { if (isEditing()) { cellEditor.stopCellEditing(); } super.columnMoved(e); } /** Necessary to properly stopping the editing when a column is resized. */ @Override public void columnMarginChanged(ChangeEvent e) { if (isEditing()) { cellEditor.stopCellEditing(); } super.columnMarginChanged(e); } public boolean shouldUseColoredCellRenderer() { return this.useColoredCellRenderer; } @Override public TableCellRenderer getCellRenderer(int row, int col) { if (useColoredCellRenderer) { Color color = null; CellColorProvider usedColorProvider = getCellColorProvider(); if (usedColorProvider != null) { color = usedColorProvider.getCellColor(row, col); } if (color != null) renderer.setColor(color); renderer.setDateFormat(getDateFormat(row, convertColumnIndexToModel(col))); if (col < maximalTextLengths.length) { renderer.setMaximalTextLength(maximalTextLengths[col]); } if (col < cutOnLineBreaks.length) { renderer.setCutOnFirstLineBreak(cutOnLineBreaks[col]); } return renderer; } else { return super.getCellRenderer(row, col); } } /** This method ensures that the correct tool tip for the current table cell is delivered. */ @Override public String getToolTipText(MouseEvent e) { Point p = e.getPoint(); int colIndex = columnAtPoint(p); int rowIndex = rowAtPoint(p); return getToolTipText(colIndex, rowIndex); } protected String getToolTipText(int colIndex, int rowIndex) { int realColumnIndex = convertColumnIndexToModel(colIndex); String text = null; if (rowIndex >= 0 && rowIndex < getRowCount() && realColumnIndex >= 0 && realColumnIndex < getModel().getColumnCount()) { Object value = getModel().getValueAt(rowIndex, realColumnIndex); if (value instanceof Number) { Number number = (Number)value; double numberValue = number.doubleValue(); text = Tools.formatIntegerIfPossible(numberValue); } else { if (value != null) { if (value instanceof Date) { int dateFormat = getDateFormat(rowIndex, realColumnIndex); switch (dateFormat) { case ExtendedJTable.DATE_FORMAT: text = Tools.formatDate((Date)value); break; case ExtendedJTable.TIME_FORMAT: text = Tools.formatTime((Date)value); break; case ExtendedJTable.DATE_TIME_FORMAT: text = Tools.formatDateTime((Date)value); break; default: text = value.toString(); break; } } else { text = value.toString(); } } else { text = "?"; } } } if (text != null && !text.equals("")) { return SwingTools.transformToolTipText(text, true); } else { return super.getToolTipText(); } } /** * {@link Tableable} Method */ @Override public String getCell(int row, int column) { String text = null; if (getTableHeader() != null) { if (row == 0) { // titel row return getTableHeader().getColumnModel().getColumn(column).getHeaderValue().toString(); } else { row--; } } // data area Object value = getModel().getValueAt(row, column); if (value instanceof Number) { Number number = (Number)value; double numberValue = number.doubleValue(); text = Tools.formatIntegerIfPossible(numberValue); } else { if (value != null) text = value.toString(); else text = "?"; } return text; } /** * {@link Tableable} Method */ @Override public int getColumnNumber() { return getColumnCount(); } /** * {@link Tableable} Method */ @Override public int getRowNumber() { if (getTableHeader() != null) { return getRowCount() + 1; } else { return getRowCount(); } } /** * {@link Tableable} Method */ @Override public void prepareReporting() {} /** * {@link Tableable} Method */ @Override public void finishReporting() {} /** * {@link Tableable} Method */ @Override public boolean isFirstLineHeader() { return false; } /** * {@link Tableable} Method */ @Override public boolean isFirstColumnHeader() { return false; } /** * Converts the index of the row in the view to the corresponding row in the original model. * They might difer if the table is sorted. * @param rowIndex The index of the row in the view. * @return The index of the row in the original model. */ public int getModelIndex(int rowIndex) { if (tableSorter != null) { return tableSorter.modelIndex(rowIndex); } return rowIndex; } public void setExtendedScrollPane(ExtendedJScrollPane scrollPane) { this.scrollPaneParent = scrollPane; } public ExtendedJScrollPane getExtendedScrollPane() { return this.scrollPaneParent; } public void selectCompleteRow() { addColumnSelectionInterval(0, getColumnCount() - 1); } public void selectCompleteColumn() { addRowSelectionInterval(0, getRowCount() - 1); } @Override public void mouseEntered(MouseEvent e) {} @Override public void mouseExited(MouseEvent e) {} @Override public void mouseClicked(MouseEvent e) { mouseReleased(e); } @Override public void mousePressed(MouseEvent e) { mouseReleased(e); } @Override public void mouseReleased(MouseEvent e) { if (showPopopUpMenu) { if (e.isPopupTrigger()) { Point p = e.getPoint(); int row = rowAtPoint(p); int c = columnAtPoint(p); int column = convertColumnIndexToModel(c); setRowSelectionInterval(row, row); setColumnSelectionInterval(column, column); JPopupMenu menu = createPopupMenu(); menu.show(this, e.getX(), e.getY()); } } } public JPopupMenu createPopupMenu() { JPopupMenu menu = new JPopupMenu(); populatePopupMenu(menu); return menu; } public void populatePopupMenu(JPopupMenu menu) { menu.add(ROW_ACTION); menu.add(COLUMN_ACTION); if (getTableHeader() != null) { menu.addSeparator(); menu.add(FIT_COLUMN_ACTION); menu.add(FIT_ALL_COLUMNS_ACTION); menu.add(EQUAL_WIDTHS_ACTION); } if (isSortable()) { menu.addSeparator(); menu.add(SORTING_ASCENDING_ACTION); menu.add(SORTING_DESCENDING_ACTION); menu.addSeparator(); menu.add(ADD_TO_SORTING_ASCENDING_ACTION); menu.add(ADD_TO_SORTING_DESCENDING_ACTION); } if (getTableHeader() != null) { if (getTableHeader().getReorderingAllowed()) { menu.addSeparator(); menu.add(SORT_COLUMNS_BY_NAME_ACTION); menu.add(RESTORE_COLUMN_ORDER_ACTION); menu.add(generateMoveColumnMenu()); } } } private JMenu generateMoveColumnMenu() { JMenu subMenu = new ResourceMenu("move_column_menu"); int first = 0; if (this.fixFirstColumn) { first = 1; } for (int i = first; i < this.getColumnCount(); i++) { subMenu.add(new MoveColumnAction(this, IconSize.SMALL, i)); } return subMenu; } private class TableToolTipProvider implements TipProvider { @Override public Component getCustomComponent(Object id) { return null; } @Override public Object getIdUnder(Point point) { Pair<Integer, Integer> cellId = new Pair<Integer, Integer>(columnAtPoint(point), rowAtPoint(point)); return cellId; } @SuppressWarnings("unchecked") @Override public String getTip(Object id) { Pair<Integer, Integer> cellId = (Pair<Integer, Integer>) id; return getToolTipText(cellId.getFirst(), cellId.getSecond()); } } }