org.netbeans.swing.etable.ETable Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.swing.etable;
import java.awt.AWTKeyStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.ContainerOrderFocusTraversalPolicy;
import java.awt.Event;
import java.awt.EventQueue;
import java.awt.FocusTraversalPolicy;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.EventObject;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListSelectionModel;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.RowSorter;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TableModelEvent;
import javax.swing.table.*;
import org.netbeans.swing.outline.DefaultOutlineModel;
import org.openide.util.ImageUtilities;
/**
* Extended JTable (ETable) adds these features to JTable:
*
* - The notion of fully editable (non-editable) table.
* - Sorting the rows of the table by clicking the header.
* Shift-Click allows to use more columns for the sort. The sort is
* based on the values implementing Comparable interface.
* - Automatic column width after init or after
* the model is changed (or triggered by "Ctrl-+" shortcut).
* Automatic resize the column after double-click
* in the header column divider area.
* - Persistence of the user customized settings via
* methods readSettings and writeSettings.
*
- Quick-Filter features allowing to show only
* certain rows from the model (see setQuickFilter()).
*
*
* @author David Strupl
*/
public class ETable extends JTable {
private static final Logger LOG = Logger.getLogger(ETable.class.getName());
/** Property that is fired when calling {@link #setQuickFilter(int, java.lang.Object)} or
* {@link #unsetQuickFilter()}.
* @since 1.13
*/
public static final String PROP_QUICK_FILTER = "quickFilter";
/** Action key for up/down focus action */
private static final String ACTION_FOCUS_NEXT = "focusNext"; //NOI18N
/** Possible value for editing property */
private static final int FULLY_EDITABLE = 1;
/** Possible value for editing property */
private static final int FULLY_NONEDITABLE = 2;
/** Possible value for editing property */
private static final int DEFAULT = 3;
/** Key for storing the currently searched column's index. */
private static final String SEARCH_COLUMN = "SearchColumn";
// icon of column button
private static final String DEFAULT_COLUMNS_ICON = "org/netbeans/swing/etable/columns.gif"; // NOI18N
/**
* Property allowing to make the table FULLY_NONEDITABLE and
* FULLY_EDITABLE.
*/
private int editing = DEFAULT;
private boolean sortable = true; // ETable is sortable by default.
/**
* Array with size exactly same as the number of rows in the data model
* or null. If it is not null the row originally at index i will be
* displayed on index sortingPermutation[i].
*/
protected transient int [] sortingPermutation;
/** Inverse of the above */
protected transient int [] inverseSortingPermutation;
/**
*
*/
private transient int filteredRowCount;
/**
*
*/
private Object quickFilterObject;
/**
*
*/
private int quickFilterColumn = -1;
// Search text field related variables:
/** */
private String maxPrefix;
/** */
int SEARCH_FIELD_PREFERRED_SIZE = 160;
/** */
int SEARCH_FIELD_SPACE = 3;
/** */
private final JTextField searchTextField = new SearchTextField();
/** */
private final int heightOfTextField = searchTextField.getPreferredSize().height;
/** */
private JPanel searchPanel = null;
/** */
private JComboBox searchCombo = null;
/** */
private ETableColumn searchColumn = null;
/**
* This text can be customized using {@link #setSelectVisibleColumnsLabel(java.lang.String)} method.
*/
private String selectVisibleColumnsLabel =
java.util.ResourceBundle.getBundle("org/netbeans/swing/etable/Bundle").getString("LBL_SelectVisibleColumns");
private boolean inEditRequest = false;
private boolean inRemoveRequest=false;
private static String COMPUTING_TOOLTIP = "ComputingTooltip";
/**
* Default formatting strings for the Quick Filter feature.
* Can be customized by setQuickFilterFormatStrings(...) method.
*/
private String[] quickFilterFormatStrings = new String [] {
"{0} == {1}", "{0} <> {1}", "{0} > {1}",
"{0} < {1}", "{0} >= {1}", "{0} <= {1}",
java.util.ResourceBundle.getBundle("org/netbeans/swing/etable/Bundle").getString("LBL_NoFilter")
};
/**
* Listener reacting to the user clicks on the header.
*/
private MouseListener headerMouseListener = new HeaderMouseListener();
/**
* Listener reacting to the user clicks on the table invoking the
* column selection dialog.
*/
private MouseListener columnSelectionMouseListener = new ColumnSelectionMouseListener();
/**
* Allows to supply alternative implementation of the column
* selection functionality in ETable.
*/
private TableColumnSelector columnSelector;
/**
* Allows to supply alternative implementation of the column
* selection functionality globally for all instances.
*/
private static TableColumnSelector defaultColumnSelector;
private final Object columnSelectionOnMouseClickLock = new Object();
private ColumnSelection[] columnSelectionOnMouseClick = new ColumnSelection[] {
ColumnSelection.NO_SELECTION, // no button
ColumnSelection.DIALOG, // dialog on left-click
ColumnSelection.NO_SELECTION, // no action on middle button
ColumnSelection.POPUP, // popup on right-click
};
private boolean columnHidingAllowed = true;
/**
* The visible column selection methods.
* @since 1.17
*/
public enum ColumnSelection {
NO_SELECTION, POPUP, DIALOG
}
/**
* Constructs a default JTable
that is initialized with a default
* data model, a default column model, and a default selection
* model.
*
* @see #createDefaultDataModel
* @see #createDefaultColumnModel
* @see #createDefaultSelectionModel
*/
public ETable() {
updateMouseListener();
}
/**
* Constructs a JTable
that is initialized with
* dm
as the data model, a default column model,
* and a default selection model.
*
* @param dm the data model for the table
* @see #createDefaultColumnModel
* @see #createDefaultSelectionModel
*/
public ETable(TableModel dm) {
super(dm);
updateMouseListener();
}
/**
* Constructs a JTable
that is initialized with
* dm
as the data model, cm
* as the column model, and a default selection model.
*
* @param dm the data model for the table
* @param cm the column model for the table
* @see #createDefaultSelectionModel
*/
public ETable(TableModel dm, TableColumnModel cm) {
super(dm, cm);
updateMouseListener();
}
/**
* Constructs a JTable
that is initialized with
* dm
as the data model, cm
as the
* column model, and sm
as the selection model.
* If any of the parameters are null
this method
* will initialize the table with the corresponding default model.
* The autoCreateColumnsFromModel
flag is set to false
* if cm
is non-null, otherwise it is set to true
* and the column model is populated with suitable
* TableColumns
for the columns in dm
.
*
* @param dm the data model for the table
* @param cm the column model for the table
* @param sm the row selection model for the table
* @see #createDefaultDataModel
* @see #createDefaultColumnModel
* @see #createDefaultSelectionModel
*/
public ETable(TableModel dm, TableColumnModel cm, ListSelectionModel sm) {
super(dm, cm, sm);
updateMouseListener();
}
/**
* Constructs a JTable
with numRows
* and numColumns
of empty cells using
* DefaultTableModel
. The columns will have
* names of the form "A", "B", "C", etc.
*
* @param numRows the number of rows the table holds
* @param numColumns the number of columns the table holds
* @see javax.swing.table.DefaultTableModel
*/
public ETable(int numRows, int numColumns) {
super(numRows, numColumns);
updateMouseListener();
}
/**
* Constructs a JTable
to display the values in the
* Vector
of Vectors
, rowData
,
* with column names, columnNames
. The
* Vectors
contained in rowData
* should contain the values for that row. In other words,
* the value of the cell at row 1, column 5 can be obtained
* with the following code:
* ((Vector)rowData.elementAt(1)).elementAt(5);
*
* @param rowData the data for the new table
* @param columnNames names of each column
*/
public ETable(Vector rowData, Vector columnNames) {
super(rowData, columnNames);
updateMouseListener();
}
/**
* Constructs a JTable
to display the values in the two dimensional array,
* rowData
, with column names, columnNames
.
* rowData
is an array of rows, so the value of the cell at row 1,
* column 5 can be obtained with the following code:
*
rowData[1][5];
*
* All rows must be of the same length as columnNames
.
*
* @param rowData the data for the new table
* @param columnNames names of each column
*/
public ETable(final Object[][] rowData, final Object[] columnNames) {
super(rowData, columnNames);
updateMouseListener();
}
@Override
protected ListSelectionModel createDefaultSelectionModel() {
return new ETableSelectionModel();
}
/**
* Returns true if the cell at row
and column
* is editable. Otherwise, invoking setValueAt
on the cell
* will have no effect.
*
* Returns true always if the ETable
is fully editable.
*
* Returns false always if the ETable
is fully non-editable.
*
* @param row the row whose value is to be queried
* @param column the column whose value is to be queried
* @return true if the cell is editable
* @see #setValueAt
* @see #setFullyEditable
* @see #setFullyNonEditable
*/
@Override
public boolean isCellEditable(int row, int column) {
if(editing == FULLY_EDITABLE) {
return true;
}
if(editing == FULLY_NONEDITABLE) {
return false;
}
return super.isCellEditable(row, column);
}
/*
* Overridden to call convertRowIndexToModel(...).
* @see javax.swing.JTable#getCellRenderer(int, int)
*
* NOT NECESSARY - JTable does not use the "row" argument.
*
@Override
public TableCellRenderer getCellRenderer(int row, int column) {
int modelRow = convertRowIndexToModel(row);
return super.getCellRenderer(modelRow, column);
}
*/
/*
* Overridden to call convertRowIndexToModel(...).
* @see javax.swing.JTable#getCellEditor(int, int)
*
* NOT NECESSARY - JTable does not use the "row" argument.
*
@Override
public TableCellEditor getCellEditor(int row, int column) {
int modelRow = convertRowIndexToModel(row);
return super.getCellEditor(modelRow, column);
}
*/
/**
* Sets all the cells in the ETable
to be editable if
* fullyEditable
is true.
* if fullyEditable
is false, sets the table cells into
* their default state as in JTable.
*
* @param fullyEditable true if the table is meant to be fully editable.
* false if the table is meant to take the defalut
* state for editing.
* @see #isFullyEditable()
*/
public void setFullyEditable(boolean fullyEditable) {
if (fullyEditable) {
editing = FULLY_EDITABLE;
if(!getShowHorizontalLines()) {
setShowHorizontalLines(true);
}
Color colorBorderAllEditable = UIManager.getColor("Table.borderAllEditable");
Border border = null;
if (colorBorderAllEditable != null) {
border = BorderFactory.createLineBorder(colorBorderAllEditable);
} else {
border = BorderFactory.createLineBorder(Color.GRAY);
}
Border filler = BorderFactory.createLineBorder(getBackground());
CompoundBorder compound = new CompoundBorder(border, filler);
setBorder(new CompoundBorder(compound, border));
} else {
editing = DEFAULT;
setBorder( null );
}
Color c = UIManager.getColor("Table.defaultGrid");
if (c != null) {
setGridColor(c);
}
if (isFullyNonEditable()) {
setupSearch();
}
}
/**
* Sets all the cells in the ETable
to be non-editable if
* fullyNonEditable
is true.
* If fullyNonEditable
is false, sets the table cells into
* their default state as in JTable
.
*
* @param fullyNonEditable true if the table is meant to be fully non-editable.
* false if the table is meant to take the defalut
* state for editing.
* @see #isFullyNonEditable
*/
public void setFullyNonEditable(boolean fullyNonEditable) {
if (fullyNonEditable) {
editing = FULLY_NONEDITABLE;
if(getShowHorizontalLines())
setShowHorizontalLines(false);
Color lineBorderColor = UIManager.getColor("Table.border");
if (lineBorderColor == null) {
lineBorderColor = Color.GRAY;
}
setBorder(BorderFactory.createLineBorder(lineBorderColor));
Color c = UIManager.getColor("Table.noneditableGrid");
if (c != null) {
setGridColor(c);
}
} else {
editing = DEFAULT;
setBorder( null );
if(!getShowHorizontalLines())
setShowHorizontalLines(true);
Color defaultGridColor = UIManager.getColor("Table.defaultGrid");
if (defaultGridColor != null) {
setGridColor(defaultGridColor);
}
}
if (isFullyNonEditable()) {
setupSearch();
}
}
/**
* Returns true if ETable
is fully editable.
*
* @return true if the the table is fully editable.
* @see #setFullyEditable
*/
public boolean isFullyEditable() {
return editing == FULLY_EDITABLE;
}
/**
* Returns true if ETable
is fully non-editable.
*
* @return true if the the table is fully non-editable.
* @see #setFullyNonEditable
*/
public boolean isFullyNonEditable() {
return editing == FULLY_NONEDITABLE;
}
/**
* Sets the table cell background colors accodring to NET UI guidelines.
*
* This is needed in case where the user does not use the NET Look and Feel,
* but still wants to paint the cell background colors accoring to NET L&F.
*
* This needs to be called also in case where the user has custom table cell
* renderer (that is not a DefaultTableCellRenderer
or a
* sub-class of it) for a cell even though NET L&F package is used, if the
* cell background colors need to be consistent for the custom renderer.
*
* @param renderer the custom cell renderer to be painted
* @param isSelected true if the custom cell is selected
* @param row the row, the custom cell corresponds to
* @param column the column, the custom cell corresponds to
*/
public void setCellBackground(Component renderer, boolean isSelected,
int row, int column) {
Color c = null;
if (row%2 == 0) { //Background 2
if(isSelected) {
c = UIManager.getColor("Table.selectionBackground2");
} else {
c = UIManager.getColor("Table.background2");
}
} else { // Background 1
if(isSelected) {
c = UIManager.getColor("Table.selectionBackground1");
} else {
c = UIManager.getColor("Table.background1");
}
}
if (c != null) {
renderer.setBackground(c);
}
}
/**
* Overridden to use ETableColumns instead of the original TableColumns.
* @see javax.swing.JTable#createDefaultColumnModel()
*/
@Override
public void createDefaultColumnsFromModel() {
TableModel model = getModel();
if (model != null) {
firePropertyChange("createdDefaultColumnsFromModel", null, null);
TableColumnModel colModel = getColumnModel();
int modelColumnCount = model.getColumnCount();
List oldHiddenColumns = null;
int[] hiddenColumnIndexes = null;
int[] sortedColumnIndexes = null;
if (colModel instanceof ETableColumnModel) {
ETableColumnModel etcm = (ETableColumnModel)colModel;
oldHiddenColumns = etcm.hiddenColumns;
hiddenColumnIndexes = new int[oldHiddenColumns.size()];
for (int hci = 0; hci < hiddenColumnIndexes.length; hci++) {
Object name = oldHiddenColumns.get(hci).getHeaderValue();
int index = -1;
if (name != null) {
for (int i = 0; i < modelColumnCount; i++) {
if (name.equals(model.getColumnName(i))) {
index = i;
break;
}
}
}
hiddenColumnIndexes[hci] = index;
}
List sortedColumns = (sortable) ? etcm.getSortedColumns() : Collections.emptyList();
int ns = sortedColumns.size();
if (ns > 0) {
sortedColumnIndexes = new int[ns];
for (int sci = 0; sci < ns; sci++) {
Object name = sortedColumns.get(sci).getHeaderValue();
int index = -1;
if (name != null) {
for (int i = 0; i < modelColumnCount; i++) {
if (name.equals(model.getColumnName(i))) {
index = i;
break;
}
}
}
sortedColumnIndexes[sci] = index;
}
}
}
TableColumn newColumns[] = new TableColumn[modelColumnCount];
int oi = 0;
class Sorting {
boolean ascending;
int sortRank;
Sorting(boolean ascending, int sortRank) {
this.ascending = ascending;
this.sortRank = sortRank;
}
}
Sorting[] columnSorting = new Sorting[modelColumnCount];
for (int i = 0; i < modelColumnCount; i++) {
newColumns[i] = createColumn(i);
boolean isHidden = false;
for (int hci = 0; hci < hiddenColumnIndexes.length; hci++) {
if (hiddenColumnIndexes[hci] == i) {
isHidden = true;
}
}
if (oi < colModel.getColumnCount()) {
TableColumn tc = colModel.getColumn(oi++);
if (!isHidden) {
newColumns[i].setPreferredWidth(tc.getPreferredWidth());
newColumns[i].setWidth(tc.getWidth());
}
if (sortable && tc instanceof ETableColumn && newColumns[i] instanceof ETableColumn) {
ETableColumn etc = (ETableColumn) tc;
ETableColumn enc = (ETableColumn) newColumns[i];
enc.nestedComparator = etc.nestedComparator;
if (enc.isSortingAllowed()) {
columnSorting[i] = new Sorting(etc.isAscending(), etc.getSortRank());
}
}
}
}
for (int cc = colModel.getColumnCount(); cc > 0; ) {
colModel.removeColumn(colModel.getColumn(--cc));
}
if (colModel instanceof ETableColumnModel) {
ETableColumnModel etcm = (ETableColumnModel)colModel;
etcm.hiddenColumns = new ArrayList();
etcm.hiddenColumnsPosition = new ArrayList();
etcm.clearSortedColumns();
}
for (int i = 0; i < newColumns.length; i++) {
addColumn(newColumns[i]);
}
if (colModel instanceof ETableColumnModel) {
ETableColumnModel etcm = (ETableColumnModel) colModel;
if (hiddenColumnIndexes != null) {
for (int hci = 0; hci < hiddenColumnIndexes.length; hci++) {
int index = hiddenColumnIndexes[hci];
if (index >= 0) {
etcm.setColumnHidden(newColumns[index], true);
}
}
}
if (sortedColumnIndexes != null) {
for (int sci = 0; sci < sortedColumnIndexes.length; sci++) {
int index = sortedColumnIndexes[sci];
if (index >= 0) {
Sorting sorting = columnSorting[index];
if (newColumns[index] instanceof ETableColumn && sorting != null) {
ETableColumn etc = (ETableColumn) newColumns[index];
etcm.setColumnSorted(etc, sorting.ascending, sorting.sortRank);
}
}
}
}
}
firePropertyChange("createdDefaultColumnsFromModel", null, newColumns);
}
}
/**
* Returns string used to delimit entries when
* copying into clipboard. The default implementation
* returns new line character ("\n") if the line
* argument is true. If it is false the
* tab character ("\t") is returned.
*/
public String getTransferDelimiter(boolean line) {
if (line) {
return "\n";
}
return "\t";
}
/**
* Used when copying into clipboard. The value
* passed to this method is obtained by calling
* getValueAt(...). The resulting string is put
* into clipboard. The default implementation returns
* an empty string ("") if the value is null
* and value.toString() otherwise. The method
* transformValue(value)
is called prior
* to the string conversion.
*/
public String convertValueToString(Object value) {
value = transformValue(value);
if (value == null) {
return "";
}
return value.toString();
}
/**
* Allow to plug own TableColumn implementation.
* This implementation returns ETableColumn.
* Called from createDefaultColumnsFromModel().
*/
protected TableColumn createColumn(int modelIndex) {
return new ETableColumn(modelIndex, this);
}
/**
* Overridden to use ETableColumnModel as TableColumnModel.
* @see javax.swing.JTable#createDefaultColumnModel()
*/
@Override
protected TableColumnModel createDefaultColumnModel() {
return new ETableColumnModel();
}
/**
* Overridden to call convertRowIndexToModel(...).
* @see javax.swing.JTable#getValueAt(int, int)
*/
@Override
public Object getValueAt(int row, int column) {
int modelRow = row;
return super.getValueAt(modelRow, column);
}
/**
* Overridden to call convertRowIndexToModel(...).
* @see javax.swing.JTable#setValueAt(Object, int, int)
*/
@Override
public void setValueAt(Object aValue, int row, int column) {
super.setValueAt(aValue, row, column);
}
/**
* If the quick-filter is applied the number of rows do not
* match the number of rows in the model.
*/
@Override
public int getRowCount() {
if ((quickFilterColumn != -1) && (quickFilterObject != null)) {
if (filteredRowCount == -1) {
computeFilteredRowCount();
}
return filteredRowCount;
}
return super.getRowCount();
}
/**
* Makes the table display only the rows that match the given "quick-filter".
* Filtering is done according to values from column with index column and
* according to filterObject. There are 2 possibilities for the filterObject
* parameter
* - filterObject implements QuickFilter
* interface: the method
accept(Object)
* of the QuickFilter is called to determine whether the
* row will be shown
* - if filterObject does not implement the interface the value
* is compared using method equals(Object) with the filterObject.
* If they are equal the row will be shown.
*
*/
public void setQuickFilter(int column, Object filterObject) {
quickFilterColumn = column;
quickFilterObject = filterObject;
resetPermutation ();
filteredRowCount = -1; // force to recompute the rowCount
super.tableChanged(new TableModelEvent(getModel()));
firePropertyChange(PROP_QUICK_FILTER, null, null);
}
/**
* Get the "quick-filter" object that is currently active.
*
* @return the filter object or null
* @see #setQuickFilter(int, java.lang.Object)
*/
public Object getQuickFilterObject() {
return quickFilterObject;
}
/**
* Get the column which is currently filtered by "quick-filter" object.
*
* @return the filtered column or -1
* @see #setQuickFilter(int, java.lang.Object)
*/
public int getQuickFilterColumn() {
return quickFilterColumn;
}
/**
* Makes the table show all the rows, resetting the filter state
* (to no filter).
*/
public void unsetQuickFilter() {
quickFilterObject = null;
quickFilterColumn = -1;
filteredRowCount = -1;
resetPermutation ();
super.tableChanged(new TableModelEvent(getModel()));
firePropertyChange(PROP_QUICK_FILTER, null, null);
}
/**
* Overridden to update the header listeners and also to adjust the
* preferred width of the columns.
* @see javax.swing.JTable#setModel(TableModel)
*/
@Override
public void setModel(TableModel dataModel) {
super.setModel(dataModel);
// force recomputation
filteredRowCount = -1;
resetPermutation ();
quickFilterColumn = -1;
quickFilterObject = null;
updateMouseListener();
if (defaultRenderersByColumnClass != null) {
updatePreferredWidths();
}
}
/**
* Overridden to make a speed optimization.
*/
@Override
public String getToolTipText(MouseEvent event) {
try {
putClientProperty(COMPUTING_TOOLTIP, Boolean.TRUE);
return super.getToolTipText(event);
} finally {
putClientProperty(COMPUTING_TOOLTIP, Boolean.FALSE);
}
}
/**
* Test if the column hiding is allowed.
* @return True if column hiding is allowed.
*/
public boolean isColumnHidingAllowed() {
return columnHidingAllowed;
}
/**
* Turn column hiding on/off
* @param allowColumnHiding false to turn column hiding off
*/
public void setColumnHidingAllowed( boolean allowColumnHiding ) {
if( allowColumnHiding != this.columnHidingAllowed ) {
this.columnHidingAllowed = allowColumnHiding;
configureEnclosingScrollPane();
}
}
/**
* Overridden to do additional initialization.
* @see javax.swing.JTable#initializeLocalVars()
*/
@Override
protected void initializeLocalVars() {
super.initializeLocalVars();
updatePreferredWidths();
setSurrendersFocusOnKeystroke(true);
setFocusCycleRoot(true);
setFocusTraversalPolicy(new STPolicy());
putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
putClientProperty("JTable.autoStartsEdit", Boolean.FALSE);
Set emptySet = Collections.emptySet();
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, emptySet);
setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, emptySet);
setFocusCycleRoot(false);
//Next two lines do not work using inputmap/actionmap, but do work
//using the older API. We will process ENTER to skip to next row,
//not next cell
unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, Event.SHIFT_MASK));
InputMap imp = getInputMap(WHEN_FOCUSED);
ActionMap am = getActionMap();
//Issue 37919, reinstate support for up/down cycle focus transfer.
//being focus cycle root mangles this in some dialogs
imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
KeyEvent.CTRL_MASK | KeyEvent.SHIFT_MASK, false), ACTION_FOCUS_NEXT);
imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
KeyEvent.CTRL_MASK, false), ACTION_FOCUS_NEXT);
Action ctrlTab = new CTRLTabAction();
am.put(ACTION_FOCUS_NEXT, ctrlTab);
imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0,
false), "beginEdit");
getActionMap().put("beginEdit", new EditAction());
imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0,
false), "cancelEdit");
getActionMap().put("cancelEdit", new CancelEditAction());
imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0,
false), "enter");
getActionMap().put("enter", new EnterAction());
imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "next");
imp.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
KeyEvent.SHIFT_DOWN_MASK), "previous");
am.put("next", new NavigationAction(true));
am.put("previous", new NavigationAction(false));
setTransferHandler(new ETableTransferHandler());
}
/**
* Overridden to implement CTRL-+ for resizing of all columns,
* CTRL-- for clearing the quick filter and CTRL-* for invoking the
* column selection dialog.
* @see javax.swing.JTable#processKeyBinding(KeyStroke, KeyEvent, int, boolean)
*/
@Override
protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
int condition, boolean pressed) {
// This is here because the standard way using input map and action map
// did not work since the event was "eaten" by the code in JTable that
// forwards it to the CellEditor (the code resides in the
// super.processKeyBinding method).
if (pressed) {
if (e.getKeyChar() == '+' && ( (e.getModifiers() & InputEvent.CTRL_MASK) == InputEvent.CTRL_MASK)) {
updatePreferredWidths();
e.consume();
return true;
}
if (e.getKeyChar() == '-' && ( (e.getModifiers() & InputEvent.CTRL_MASK) == InputEvent.CTRL_MASK)) {
unsetQuickFilter();
e.consume();
return true;
}
if (e.getKeyChar() == '*' && ( (e.getModifiers() & InputEvent.CTRL_MASK) == InputEvent.CTRL_MASK)) {
ColumnSelectionPanel.showColumnSelectionDialog(this);
e.consume();
return true;
}
}
boolean retValue = super.processKeyBinding(ks, e, condition, pressed);
return retValue;
}
/**
* Make the column sorted. Value of columnIndex is in the model coordinates.
* Be careful with the columnIndes parameter: again, it
* is in the model coordinates.
* @param columnIndex column in ETable column model
* @param ascending true means ascending
* @param rank value 1 means that this is the most important sorted
* column, number 2 means second etc. Value 0 means that this column
* is not sorted.
*/
public void setColumnSorted(int columnIndex, boolean ascending, int rank) {
int ii = convertColumnIndexToView(columnIndex);
if (ii < 0) {
return;
}
sortable = true;
TableColumnModel tcm = getColumnModel();
if (tcm instanceof ETableColumnModel) {
ETableColumnModel etcm = (ETableColumnModel)tcm;
TableColumn tc = tcm.getColumn(ii);
if (tc instanceof ETableColumn) {
ETableColumn etc = (ETableColumn)tc;
if (! etc.isSortingAllowed()) {
return;
}
SelectedRows selectedRows;
int wasSelectedColumn;
if (getUpdateSelectionOnSort()) {
selectedRows = getSelectedRowsInModel();
wasSelectedColumn = getSelectedColumn();
} else {
selectedRows = null;
wasSelectedColumn = -1;
}
etcm.setColumnSorted(etc, ascending, rank);
resetPermutation ();
ETable.super.tableChanged(new TableModelEvent(getModel(), -1, getRowCount()));
if (selectedRows != null) {
changeSelectionInModel(selectedRows, wasSelectedColumn);
}
}
}
}
/**
* Overridden to install special button into the upper right hand corner.
* @see javax.swing.JTable#configureEnclosingScrollPane()
*/
@Override
protected void configureEnclosingScrollPane() {
super.configureEnclosingScrollPane();
if (isFullyNonEditable()) {
setupSearch();
}
Container p = getParent();
if (p instanceof JViewport) {
Container gp = p.getParent();
if (gp instanceof JScrollPane) {
JScrollPane scrollPane = (JScrollPane)gp;
// Make certain we are the viewPort's view and not, for
// example, the rowHeaderView of the scrollPane -
// an implementor of fixed columns might do this.
JViewport viewport = scrollPane.getViewport();
if (viewport == null || viewport.getView() != this) {
return;
}
if( isColumnHidingAllowed() ) {
Icon ii = UIManager.getIcon("Table.columnSelection");
if (ii == null) {
ii = ImageUtilities.image2Icon(ImageUtilities.loadImage(DEFAULT_COLUMNS_ICON));
}
final JButton b = new JButton(ii);
// For HiDPI support.
b.setDisabledIcon(ImageUtilities.createDisabledIcon(ii));
b.setToolTipText(selectVisibleColumnsLabel);
b.getAccessibleContext().setAccessibleName(selectVisibleColumnsLabel);
b.getAccessibleContext().setAccessibleDescription(selectVisibleColumnsLabel);
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
ColumnSelectionPanel.showColumnSelectionPopupOrDialog(b, ETable.this);
}
});
b.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent me) {
ColumnSelection cs = getColumnSelectionOn(me.getButton());
switch (cs) {
case POPUP:
ColumnSelectionPanel.showColumnSelectionPopup (b, ETable.this);
break;
case DIALOG:
ColumnSelectionPanel.showColumnSelectionDialog(ETable.this);
break;
}
}
});
b.setFocusable(false);
scrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER, b);
} else {
scrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER, null);
}
}
}
updateColumnSelectionMouseListener();
}
/**
* Convert indices of selected rows to model.
*/
private SelectedRows getSelectedRowsInModel() {
SelectedRows sr = new SelectedRows();
int rows[] = getSelectedRows();
sr.rowsInView = rows;
int[] modelRows = new int[rows.length];
for (int i = 0; i < rows.length; i++) {
modelRows[i] = convertRowIndexToModel(rows[i]);
}
//System.err.println("getSelectedRowsInModel() sets new rows from view: "+
// Arrays.toString(rows)+" to model rows: "+Arrays.toString(modelRows));
sr.rowsInModel = modelRows;
ListSelectionModel rsm = getSelectionModel();
sr.anchorInView = rsm.getAnchorSelectionIndex();
sr.leadInView = rsm.getLeadSelectionIndex();
int rc = getRowCount();
sr.anchorInModel = (sr.anchorInView < rc) ? convertRowIndexToModel(sr.anchorInView) : -1;
sr.leadInModel = (sr.leadInView < rc) ? convertRowIndexToModel(sr.leadInView) : -1;
return sr;
}
/**
* Selects given rows (the rows coordinates are the model's space).
*/
private void changeSelectionInModel(SelectedRows selectedRows/*int selectedRows[]*/, int selectedColumn) {
//System.err.println("changeSelectionInModel("+selectedRows+")");
boolean wasAutoScroll = getAutoscrolls();
setAutoscrolls(false);
DefaultListSelectionModel rsm = (DefaultListSelectionModel) getSelectionModel();
rsm.setValueIsAdjusting(true);
ListSelectionModel csm = getColumnModel().getSelectionModel();
csm.setValueIsAdjusting(true);
//System.err.println("Orig lead and anchor selection indexes: "+rsm.getLeadSelectionIndex()+", "+rsm.getAnchorSelectionIndex());
int leadRow = convertRowIndexToView(selectedRows.leadInModel);
int anchorRow = convertRowIndexToView(selectedRows.anchorInModel);
Arrays.sort(selectedRows.rowsInView);
//System.err.println("OrigSelectedRows in view = "+Arrays.toString(selectedRows.rowsInView));
int[] newRowsInView = new int[selectedRows.rowsInModel.length];
int n = getModel().getRowCount();
for (int i = 0; i < selectedRows.rowsInModel.length; i++) {
if (i < selectedRows.rowsInView.length &&
((selectedRows.rowsInView[i] < 0) || (selectedRows.rowsInView[i] >= n))) {
newRowsInView[i] = -1;
continue;
}
int viewIndex = convertRowIndexToView(selectedRows.rowsInModel[i]);
newRowsInView[i] = viewIndex;
}
Arrays.sort(newRowsInView);
//System.err.println("NewSelectedRows in view = "+Arrays.toString(newRowsInView));
int[] rowsInView = selectedRows.rowsInView;
int i = 0;
int j = 0;
while (i < rowsInView.length || j < newRowsInView.length) {
int selected = (i < rowsInView.length) ? rowsInView[i] : Integer.MAX_VALUE;
int toSelect = (j < newRowsInView.length) ? newRowsInView[j] : Integer.MAX_VALUE;
if (selected == toSelect) {
i++;
j++;
continue;
}
if (selected < toSelect) {
if (selected > -1) {
int selected2 = selected;
while ((i + 1) < rowsInView.length && rowsInView[i+1] == (selected2 + 1) && (selected2 + 1) < toSelect) {
selected2++;
i++;
}
rsm.removeSelectionInterval(selected, selected2);
//System.err.println(" removing selection ("+selected+", "+selected2+")");
}
i++;
} else {
if (toSelect > -1) {
int toSelect2 = toSelect;
while ((j + 1) < newRowsInView.length && newRowsInView[j + 1] == (toSelect2 + 1) && (toSelect2 + 1) < selected) {
toSelect2++;
j++;
}
rsm.addSelectionInterval(toSelect, toSelect2);
//System.err.println(" adding selection ("+toSelect+", "+toSelect2+")");
}
j++;
}
}
if (anchorRow != selectedRows.anchorInView) {
//System.err.println(" Setting anchor selection index: "+anchorRow);
rsm.setAnchorSelectionIndex(anchorRow);
}
if (leadRow != selectedRows.leadInView) {
if (leadRow == -1) {
for (int k = 0; k < newRowsInView.length; k++) {
if (newRowsInView[k] == -1) {
while((k + 1) < newRowsInView.length && newRowsInView[k+1] == -1) {
k++;
}
if ((k + 1) < newRowsInView.length) {
leadRow = newRowsInView[k+1];
break;
}
if (k > 0) {
leadRow = newRowsInView[0];
break;
}
}
}
}
//System.err.println(" Setting lead selection index: "+leadRow);
// DO NOT CALL setLeadSelectionIndex() as it screws up selection!
rsm.moveLeadSelectionIndex(leadRow);
}
rsm.setValueIsAdjusting(false);
csm.setValueIsAdjusting(false);
if (wasAutoScroll) {
setAutoscrolls(true);
}
}
private void updateSelectedLines(int[] currentLineSelections,
int currentLeadRow, int currentAnchorRow,
int[] newLineSelections,
int newLeadRow, int newAnchorRow) {
//System.err.println("updateSelectedLines("+Arrays.toString(currentLineSelections)+" => "+
// Arrays.toString(newLineSelections));
boolean wasAutoScroll = getAutoscrolls();
setAutoscrolls(false);
DefaultListSelectionModel rsm = (DefaultListSelectionModel) getSelectionModel();
rsm.setValueIsAdjusting(true);
ListSelectionModel csm = getColumnModel().getSelectionModel();
csm.setValueIsAdjusting(true);
//System.err.println("Orig lead and anchor selection indexes: "+rsm.getLeadSelectionIndex()+", "+rsm.getAnchorSelectionIndex());
//int leadRow = convertRowIndexToView(selectedRows.leadInModel);
//int anchorRow = convertRowIndexToView(selectedRows.anchorInModel);
int i = 0;
int j = 0;
while (i < currentLineSelections.length || j < newLineSelections.length) {
int selected = (i < currentLineSelections.length) ? currentLineSelections[i] : Integer.MAX_VALUE;
int toSelect = (j < newLineSelections.length) ? newLineSelections[j] : Integer.MAX_VALUE;
if (selected == toSelect) {
i++;
j++;
continue;
}
if (selected < toSelect) {
if (selected > -1) {
int selected2 = selected;
while ((i + 1) < currentLineSelections.length && currentLineSelections[i+1] == (selected2 + 1) && (selected2 + 1) < toSelect) {
selected2++;
i++;
}
rsm.removeSelectionInterval(selected, selected2);
//System.err.println(" removing selection ("+selected+", "+selected2+")");
}
i++;
} else {
if (toSelect > -1) {
int toSelect2 = toSelect;
while ((j + 1) < newLineSelections.length && newLineSelections[j + 1] == (toSelect2 + 1) && (toSelect2 + 1) < selected) {
toSelect2++;
j++;
}
rsm.addSelectionInterval(toSelect, toSelect2);
//System.err.println(" adding selection ("+toSelect+", "+toSelect2+")");
}
j++;
}
}
if (newAnchorRow != currentAnchorRow) {
//System.err.println(" Setting anchor selection index: "+anchorRow);
rsm.setAnchorSelectionIndex(newAnchorRow);
}
if (newLeadRow != currentLeadRow) {
if (newLeadRow == -1) {
for (int k = 0; k < newLineSelections.length; k++) {
if (newLineSelections[k] == -1) {
while((k + 1) < newLineSelections.length && newLineSelections[k+1] == -1) {
k++;
}
if ((k + 1) < newLineSelections.length) {
newLeadRow = newLineSelections[k+1];
break;
}
if (k > 0) {
newLeadRow = newLineSelections[0];
break;
}
}
}
}
//System.err.println(" Setting lead selection index: "+leadRow);
// DO NOT CALL setLeadSelectionIndex() as it screws up selection!
rsm.moveLeadSelectionIndex(newLeadRow);
}
rsm.setValueIsAdjusting(false);
csm.setValueIsAdjusting(false);
if (wasAutoScroll) {
setAutoscrolls(true);
}
}
/**
* This method update mouse listener on the scrollPane if it is needed.
* It also recomputes the model of searchCombo. Both actions are needed after
* the set of visible columns is changed.
*/
void updateColumnSelectionMouseListener() {
Container p = getParent();
if (p instanceof JViewport) {
Container gp = p.getParent();
if (gp instanceof JScrollPane) {
JScrollPane scrollPane = (JScrollPane)gp;
// Make certain we are the viewPort's view and not, for
// example, the rowHeaderView of the scrollPane -
// an implementor of fixed columns might do this.
JViewport viewport = scrollPane.getViewport();
if (viewport == null || viewport.getView() != this) {
return;
}
scrollPane.removeMouseListener(columnSelectionMouseListener);
if (getColumnModel().getColumnCount() == 0) {
scrollPane.addMouseListener(columnSelectionMouseListener);
}
}
}
if (searchCombo != null) {
searchCombo.setModel(getSearchComboModel());
}
}
private SelectedRows selectedRowsWhenTableChanged = null;
private int[][] sortingPermutationsWhenTableChanged = null;
private int selectedColumnWhenTableChanged = -1;
/**
* If the table data model is changed we reset (and then recompute)
* the sorting permutation and the row count. The selection is restored
* when needed.
*/
@Override
public void tableChanged(TableModelEvent e) {
boolean needsTotalRefresh = true;
if (e == null || e.getFirstRow() == TableModelEvent.HEADER_ROW) {
resetPermutation ();
filteredRowCount = -1;
super.tableChanged(e);
return;
}
boolean areMoreEventsPending = false;
Object source = e.getSource();
if (source instanceof DefaultOutlineModel) {
areMoreEventsPending = ((DefaultOutlineModel) source).areMoreEventsPending();
}
//System.err.println("tableChanged("+e+"): e.type = "+e.getType()+", first row = "+e.getFirstRow()+", last row = "+e.getLastRow()+
// ", areMoreEventsPending = "+areMoreEventsPending);
if (e.getType() == TableModelEvent.INSERT ||
e.getType() == TableModelEvent.DELETE) {
if (selectedRowsWhenTableChanged == null) {
selectedRowsWhenTableChanged = getSelectedRowsInModel();
sortingPermutationsWhenTableChanged = new int[2][];
sortingPermutationsWhenTableChanged[0] = sortingPermutation;
sortingPermutationsWhenTableChanged[1] = inverseSortingPermutation;
selectedColumnWhenTableChanged = getSelectedColumn();
resetPermutation();
filteredRowCount = -1;
//System.err.println("selected rows in model = "+Arrays.toString(selectedRowsWhenTableChanged.rowsInModel));
}
TableModelEvent se = e;
/* Not necessary to transform, as we're still in the UI coordinates.
int[] invSortPermutation = sortingPermutationsWhenTableChanged[1];
if (invSortPermutation != null) {
int fr = e.getFirstRow();
if (fr >= 0 && fr < invSortPermutation.length) {
fr = invSortPermutation[fr];
}
int lr = e.getLastRow();
if (lr >= 0 && lr < invSortPermutation.length) {
lr = invSortPermutation[lr];
}
se = new TableModelEvent((TableModel) e.getSource(), fr, lr, e.getColumn(), e.getType());
}*/
super.tableChanged(se);
int first = e.getFirstRow();
int last = e.getLastRow();
if (first > last) {
int r = last;
last = first;
first = r;
}
int count = last - first + 1;
if (count > 0) {
int[] wasSelectedRows = selectedRowsWhenTableChanged.rowsInModel;
if (e.getType() == TableModelEvent.INSERT) {
for (int i = 0; i < wasSelectedRows.length; i++) {
if (wasSelectedRows[i] >= first) {
wasSelectedRows[i] += count;
}
}
} else {
for (int i = 0; i < wasSelectedRows.length; i++) {
if (wasSelectedRows[i] >= first) {
if (wasSelectedRows[i] <= last) {
wasSelectedRows[i] = -1;
} else {
wasSelectedRows[i] -= count;
}
}
}
}
}
if (!areMoreEventsPending) {
//System.err.println(" => after all events selecting Rows = "+selectedRowsWhenTableChanged);
if (selectedRowsWhenTableChanged.rowsInModel.length > 0) {
selectedRowsWhenTableChanged.rowsInView = getSelectedRows(); // Actual selected rows in the view
//System.err.println(" adjusted selected rows to "+selectedRowsWhenTableChanged);
changeSelectionInModel(selectedRowsWhenTableChanged, selectedColumnWhenTableChanged);
}
selectedRowsWhenTableChanged = null;
sortingPermutationsWhenTableChanged = null;
selectedColumnWhenTableChanged = -1;
}
return ;
}
int modelColumn = e.getColumn();
int start = e.getFirstRow();
int end = e.getLastRow();
//System.err.println("ETable.tableChanged("+e+"):\n modelColumn = "+modelColumn+", start = "+start+", end = "+end);
if (start != -1) {
start = convertRowIndexToView(start);
if (start == -1) {
start = 0;
}
}
if (end != -1) {
end = convertRowIndexToView(end);
if (end == -1) {
end = Integer.MAX_VALUE;
}
}
//System.err.println(" => convert: modelColumn = "+modelColumn+", start = "+start+", end = "+end);
if (start == end) {
final TableModelEvent tme = new TableModelEvent(
(TableModel)e.getSource(),
start, end, modelColumn);
super.tableChanged(tme);
return ;
}
if (modelColumn != TableModelEvent.ALL_COLUMNS) {
Enumeration enumeration = getColumnModel().getColumns();
TableColumn aColumn;
while (enumeration.hasMoreElements()) {
aColumn = enumeration.nextElement();
if (aColumn.getModelIndex() == modelColumn) {
ETableColumn etc = (ETableColumn)aColumn;
if ((! etc.isSorted()) && (quickFilterColumn != modelColumn)){
needsTotalRefresh = false;
}
}
}
}
if (needsTotalRefresh) { // update the whole table
SelectedRows selectedRows = getSelectedRowsInModel();
int wasSelectedColumn = getSelectedColumn();
//System.err.println("NEEDS TOTAL REFRESH: selectedRows = "+selectedRows);
resetPermutation ();
filteredRowCount = -1;
super.tableChanged(new TableModelEvent(getModel()));
changeSelectionInModel(selectedRows, wasSelectedColumn);
} else { // update only one column
TableModelEvent tme = new TableModelEvent(
(TableModel)e.getSource(),
0, getModel().getRowCount(), modelColumn);
super.tableChanged(tme);
}
}
private void resetPermutation () {
assert SwingUtilities.isEventDispatchThread () : "Do resetting of permutation only in AWT queue!";
/*
System.err.print(" PERMUT. RESET, old PERMUT. = ");
if (sortingPermutation == null) {
System.err.println("null");
} else {
System.err.println(Arrays.toString(sortingPermutation));
}
System.err.print(" old INV. P. = ");
if (inverseSortingPermutation == null) {
System.err.println("null");
} else {
System.err.println(Arrays.toString(inverseSortingPermutation));
}*/
sortingPermutation = null;
inverseSortingPermutation = null;
}
/**
* When the user clicks the header this method returns either
* the column that should be resized or null.
*/
private TableColumn getResizingColumn(Point p) {
JTableHeader header = getTableHeader();
if (header == null) {
return null;
}
int column = header.columnAtPoint(p);
if (column == -1) {
return null;
}
Rectangle r = header.getHeaderRect(column);
r.grow(-3, 0);
if (r.contains(p)) {
return null;
}
int midPoint = r.x + r.width/2;
int columnIndex;
if( header.getComponentOrientation().isLeftToRight() ) {
columnIndex = (p.x < midPoint) ? column - 1 : column;
} else {
columnIndex = (p.x < midPoint) ? column : column - 1;
}
if (columnIndex == -1) {
return null;
}
return header.getColumnModel().getColumn(columnIndex);
}
/**
* Adds mouse listener to the header for sorting and auto-sizing
* of the columns.
*/
private void updateMouseListener() {
JTableHeader jth = getTableHeader();
if (jth != null) {
jth.removeMouseListener(headerMouseListener); // not to add it twice
jth.addMouseListener(headerMouseListener);
}
}
@Override
protected JTableHeader createDefaultTableHeader() {
return new ETableHeader(columnModel);
}
/**
* Updates the value of filteredRowCount variable.
*/
private void computeFilteredRowCount() {
if ((quickFilterColumn == -1) || (quickFilterObject == null) ) {
filteredRowCount = -1;
return;
}
if (sortingPermutation != null) {
filteredRowCount = sortingPermutation.length;
return;
}
sortAndFilter();
if (sortingPermutation != null) {
filteredRowCount = sortingPermutation.length;
}
}
/**
* Helper method converting the row index according to the active sorting
* columns.
*/
@Override
public int convertRowIndexToModel(int row) {
if (!(getColumnModel() instanceof ETableColumnModel)) {
// Use JDK 6 sorting and filtering with custom column models.
return super.convertRowIndexToModel(row);
}
if (sortingPermutation == null) {
sortAndFilter();
}
if (sortingPermutation != null) {
if ((row >= 0) && (row < sortingPermutation.length)) {
return sortingPermutation[row];
}
if (row > 0 && row != Integer.MAX_VALUE && LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE,
"Row "+row+" is bigger than sortingPermutation.length = "+sortingPermutation.length,
new IllegalStateException("Row = "+row+", sortingPermutation = "+Arrays.toString(sortingPermutation)));
//System.err.println("Row "+row+" is bigger than sortingPermutation length, sortingPermutation = "+Arrays.toString(sortingPermutation));
}
return row;
}
return row;
}
/**
* Helper method converting the row index according to the active sorting
* columns.
*/
@Override
public int convertRowIndexToView(int row) {
if (!(getColumnModel() instanceof ETableColumnModel)) {
// Use JDK 6 sorting and filtering with custom column models.
return super.convertRowIndexToView(row);
}
if (inverseSortingPermutation == null) {
sortAndFilter();
}
if (inverseSortingPermutation != null) {
if ((row >= 0) && (row < inverseSortingPermutation.length)) {
return inverseSortingPermutation[row];
}
return -1;
}
return row;
}
/**
* Allows customization of the text appearing in the column
* customization dialog.
*/
public void setSelectVisibleColumnsLabel(String localizedLabel) {
selectVisibleColumnsLabel = localizedLabel;
}
String getSelectVisibleColumnsLabel() {
return selectVisibleColumnsLabel;
}
/**
* Replaces the quickFilterFormatStrings by the given array. The
* new array must have the same length as the old one.
*/
public void setQuickFilterFormatStrings(String []newFormats) {
if ((newFormats == null) || (newFormats.length != quickFilterFormatStrings.length)) {
return;
}
quickFilterFormatStrings = newFormats.clone();
}
/**
* Returns the quickFilterFormatStrings array.
*/
public String[] getQuickFilterFormatStrings() {
return quickFilterFormatStrings.clone();
}
/**
* Allows subclasses to localize the column headers. This method
* is called by the header renderer. The default implementation just
* returns passed in columnName.
*/
public String getColumnDisplayName(String columnName) {
return columnName;
}
/**
* Allows subclasses to transform the value (usually obtained by calling
* getValueAt(...)) to another object that is used for
* sorting, comparison via quick filters etc. The default implementation
* just returns the original value.
*/
public Object transformValue(Object value) {
return value;
}
/**
* Creates a menu item usable in a popup that will trigger
* QuickFilter functionality for given column and value of the
* cell it was invoked on (returned from
* transformValue(getValueAt(column, row))). Note: do not
* forget to call transformValue before passing the value to this method
* otherwise the quickfilters will not work.
* The label should be localized version of the string the user will
* see in the popup menu, e.g. "Filter Column".
*/
public JMenuItem getQuickFilterPopup(int column, Object value, String label) {
JMenu menu = new JMenu(label);
String columnDisplayName = getColumnDisplayName(getColumnName(column));
JMenuItem equalsItem = getQuickFilterEqualsItem(column, value,
columnDisplayName, quickFilterFormatStrings[0], true);
menu.add(equalsItem);
JMenuItem notequalsItem = getQuickFilterEqualsItem(column, value,
columnDisplayName, quickFilterFormatStrings[1], false);
menu.add(notequalsItem);
JMenuItem greaterItem = getQuickFilterCompareItem(column, value,
columnDisplayName, quickFilterFormatStrings[2], true, false);
menu.add(greaterItem);
JMenuItem lessItem = getQuickFilterCompareItem(column, value,
columnDisplayName, quickFilterFormatStrings[3], false, false);
menu.add(lessItem);
JMenuItem greaterEqualsItem = getQuickFilterCompareItem(column, value,
columnDisplayName, quickFilterFormatStrings[4], true, true);
menu.add(greaterEqualsItem);
JMenuItem lessEqualsItem = getQuickFilterCompareItem(column, value,
columnDisplayName, quickFilterFormatStrings[5], false, true);
menu.add(lessEqualsItem);
JMenuItem noFilterItem = getQuickFilterNoFilterItem(quickFilterFormatStrings[6]);
menu.add(noFilterItem);
return menu;
}
/**
* Creates the menu item for setting the quick filter that filters
* the objects using equality (or non-equality).
*/
public JMenuItem getQuickFilterEqualsItem(final int column, Object value,
String columnName, String text, boolean equals) {
String s = MessageFormat.format(text, new Object[] { columnName, value});
JMenuItem res = new JMenuItem(s);
int modelColumn = convertColumnIndexToModel(column);
res.addActionListener(new EqualsQuickFilter(modelColumn, value, equals));
return res;
}
/**
* Private implementation of the equality quick filter.
*/
private class EqualsQuickFilter implements ActionListener, QuickFilter {
private int column;
private Object value;
private boolean equals;
public EqualsQuickFilter(int column, Object value, boolean equals) {
this.column = column;
this.value = value;
this.equals = equals;
}
@Override
public boolean accept(Object aValue) {
if ((value == null) && (aValue == null)) {
return equals;
}
if ((value == null) || (aValue == null)) {
return ! equals;
}
if (equals) {
return value.equals(aValue);
} else {
return ! value.equals(aValue);
}
}
@Override
public void actionPerformed(ActionEvent actionEvent) {
setQuickFilter(column, this);
}
}
/**
* Creates the menu item for resetting the quick filter (to no filter).
*/
public JMenuItem getQuickFilterNoFilterItem(String label) {
JMenuItem res = new JMenuItem(label);
res.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
unsetQuickFilter();
}
});
return res;
}
/**
* Creates the menu item for setting the quick filter that filters
* the objects using Comparable interface.
*/
public JMenuItem getQuickFilterCompareItem(
final int column, Object value, String columnName,
String text, boolean greater, boolean equalsCounts) {
String s = MessageFormat.format(text, new Object[] { columnName, value});
JMenuItem res = new JMenuItem(s);
int modelColumn = convertColumnIndexToModel(column);
res.addActionListener(new CompareQuickFilter(modelColumn, value, greater, equalsCounts));
return res;
}
/**
* Private quick filter implementation using Comparable interface.
*/
private class CompareQuickFilter implements ActionListener, QuickFilter {
private int column;
private Object value;
private boolean greater;
private boolean equalsCounts;
public CompareQuickFilter(int column, Object value, boolean greater, boolean equalsCounts) {
this.column = column;
this.value = value;
this.greater = greater;
this.equalsCounts = equalsCounts;
}
@Override
public boolean accept(Object aValue) {
if (equalsCounts) {
if (greater) {
return doCompare(value, aValue) <= 0;
} else {
return doCompare(value, aValue) >= 0;
}
} else {
if (greater) {
return doCompare(value, aValue) < 0;
} else {
return doCompare(value, aValue) > 0;
}
}
}
@SuppressWarnings("unchecked")
private int doCompare(Object obj1, Object obj2) {
if (obj1 == null && obj2 == null) {
return 0;
}
if (obj1 == null) {
return -1;
}
if (obj2 == null) {
return 1;
}
if ((obj1 instanceof Comparable) && (obj1.getClass().isAssignableFrom(obj2.getClass()))){
Comparable c1 = (Comparable) obj1;
return c1.compareTo(obj2);
}
return obj1.toString().compareTo(obj2.toString());
}
@Override
public void actionPerformed(ActionEvent actionEvent) {
setQuickFilter(column, this);
}
}
/**
* Sorts the rows of the table.
*/
protected void sortAndFilter() {
TableColumnModel tcm = getColumnModel();
if (tcm instanceof ETableColumnModel) {
ETableColumnModel etcm = (ETableColumnModel) tcm;
Comparator c = etcm.getComparator();
if (c != null) {
TableModel model = getModel();
int noRows = model.getRowCount();
//System.err.println("ETable.sortAndFilter(): noRows = "+noRows);
List rows = new ArrayList();
for (int i = 0; i < noRows; i++) {
if (acceptByQuickFilter(model, i)) {
rows.add(new RowMapping(i, model, this));
}
}
rows.sort(c);
int [] res = new int[rows.size()];
int [] invRes = new int[noRows]; // carefull - this one is bigger!
for (int i = 0; i < res.length; i++) {
RowMapping rm = rows.get(i);
int rmi = rm.getModelRowIndex();
res[i] = rmi;
invRes[rmi] = i;
}
int[] oldRes = sortingPermutation;
int[] oldInvRes = inverseSortingPermutation;
//System.err.println(" SETTING PERMUTATION = "+Arrays.toString(res));
//System.err.println(" SETTING INV.PERMUT. = "+Arrays.toString(invRes));
sortingPermutation = res;
inverseSortingPermutation = invRes;
//adjustSelectedRows(oldRes, oldInvRes, res, invRes);
}
}
}
/**
* Adjusts selected rows when sorting changes.
*
protected final void adjustSelectedRows(int[] oldSortingPermutation, int[] oldInverseSortingPermutation,
int[] newSortingPermutation, int[] newInverseSortingPermutation) {
if (true) return ;
int[] oldSelectedRows = getSelectedRows();
int n = oldSelectedRows.length;
//System.err.println("adjustSelectedRows("+n+")");
if (n == 0) {
return ;
}
int[] newSelectedRows = new int[n];
System.arraycopy(oldSelectedRows, 0, newSelectedRows, 0, n);
for (int i = 0; i < n; i++) {
int vr = oldSelectedRows[i];
int mr;
if (oldSortingPermutation == null) {
mr = vr;
} else {
mr = oldSortingPermutation[vr];
}
vr = newInverseSortingPermutation[mr];
newSelectedRows[i] = vr;
}
ListSelectionModel rsm = getSelectionModel();
int oldAnchor = rsm.getAnchorSelectionIndex();
int oldLead = rsm.getLeadSelectionIndex();
int newAnchor;
if (oldAnchor < 0) {
newAnchor = oldAnchor;
} else {
int mr;
if (oldSortingPermutation == null) {
mr = oldAnchor;
} else {
mr = oldSortingPermutation[oldAnchor];
}
newAnchor = newInverseSortingPermutation[mr];
}
int newLead;
if (oldLead < 0) {
newLead = oldLead;
} else {
int mr;
if (oldSortingPermutation == null) {
mr = oldLead;
} else {
mr = oldSortingPermutation[oldLead];
}
newLead = newInverseSortingPermutation[mr];
}
updateSelectedLines(oldSelectedRows, oldLead, oldAnchor,
newSelectedRows, newLead, newAnchor);
}
*/
/**
* Determines whether the given row should be displayed or not.
*/
protected boolean acceptByQuickFilter(TableModel model, int row) {
if ((quickFilterColumn == -1) || (quickFilterObject == null) ) {
return true;
}
Object value = model.getValueAt(row, quickFilterColumn);
value = transformValue(value);
if (quickFilterObject instanceof QuickFilter) {
QuickFilter filter = (QuickFilter) quickFilterObject;
return filter.accept(value);
}
if (value == null) {
return false;
}
// fallback test for equality with the filter object
return value.equals(quickFilterObject);
}
/**
* Compute the preferredVidths of all columns.
*/
void updatePreferredWidths() {
Enumeration en = getColumnModel().getColumns();
while (en.hasMoreElements()) {
TableColumn obj = en.nextElement();
if (obj instanceof ETableColumn) {
ETableColumn etc = (ETableColumn) obj;
etc.updatePreferredWidth(this, false);
}
}
}
/**
* Method allowing to read stored values.
* The stored values should be only those that the user has customized,
* it does not make sense to store the values that were set using
* the initialization code because the initialization code can be run
* in the same way after restart.
*/
public void readSettings(Properties p, String propertyPrefix) {
ETableColumnModel etcm = (ETableColumnModel)createDefaultColumnModel();
etcm.readSettings(p, propertyPrefix, this);
setColumnModel(etcm);
String scs = p.getProperty(propertyPrefix + SEARCH_COLUMN);
if (scs != null) {
try {
int index = Integer.parseInt(scs);
for (int i = 0; i < etcm.getColumnCount(); i++) {
TableColumn tc = etcm.getColumn(i);
if (tc.getModelIndex() == index) {
searchColumn = (ETableColumn)tc;
break;
}
}
} catch (NumberFormatException nfe) {
nfe.printStackTrace();
}
}
filteredRowCount = -1;
resetPermutation ();
super.tableChanged(new TableModelEvent(getModel()));
}
/**
* Method allowing to store customization values.
* The stored values should be only those that the user has customized,
* it does not make sense to store the values that were set using
* the initialization code because the initialization code can be run
* in the same way after restart.
*/
public void writeSettings(Properties p, String propertyPrefix) {
TableColumnModel tcm = getColumnModel();
if (tcm instanceof ETableColumnModel) {
ETableColumnModel etcm = (ETableColumnModel) tcm;
etcm.writeSettings(p, propertyPrefix);
}
if (searchColumn != null) {
p.setProperty(
propertyPrefix + SEARCH_COLUMN,
Integer.toString(searchColumn.getModelIndex()));
}
}
/** searchTextField manages focus because it handles VK_TAB key */
private class SearchTextField extends JTextField {
@Override
@SuppressWarnings("deprecation")
public boolean isManagingFocus() {
return true;
}
@Override
public void processKeyEvent(KeyEvent ke) {
//override the default handling so that
//the parent will never receive the escape key and
//close a modal dialog
if (ke.getKeyCode() == KeyEvent.VK_ESCAPE) {
removeSearchField();
// bugfix #32909, reqest focus when search field is removed
SwingUtilities.invokeLater(new Runnable() {
//additional bugfix - do focus change later or removing
//the component while it's focused will cause focus to
//get transferred to the next component in the
//parent focusTraversalPolicy *after* our request
//focus completes, so focus goes into a black hole - Tim
@Override
public void run() {
ETable.this.requestFocus();
}
});
} else {
super.processKeyEvent(ke);
}
}
}
/**
* Searches the rows by comparing the values with the given prefix.
*/
private List doSearch(String prefix) {
List results = new ArrayList();
int startIndex = 0;
int size = getRowCount();
if ( (size == 0) || (getColumnCount() == 0)) {
// Empty table; cannot match anything.
return results;
}
int column = 0;
if (searchColumn != null) {
column = convertColumnIndexToView(searchColumn.getModelIndex());
}
if (column < 0) {
// wrong column
return results;
}
while (startIndex < size) {
Object val = getValueAt(startIndex, column);
String s = null;
if (val != null) {
s = convertValueToString(val);
}
if ((s != null) && (s.toUpperCase().indexOf(prefix.toUpperCase()))!= -1 ) {
results.add(new Integer(startIndex));
// initialize prefix
if (maxPrefix == null) {
maxPrefix = s;
}
maxPrefix = findMaxPrefix(maxPrefix, s);
}
startIndex++;
}
return results;
}
/**
* Finds maximum common prefix of 2 strings.
*/
private static String findMaxPrefix(String str1, String str2) {
int i = 0;
while (str1.regionMatches(true, 0, str2, 0, i)) {
i++;
}
i--;
if (i >= 0) {
return str1.substring(0, i);
}
return null;
}
/**
* Shows the search text field.
*/
private void setupSearch() {
// Remove the default key listeners
KeyListener keyListeners[] = (getListeners (KeyListener.class));
for (int i = 0; i < keyListeners.length; i++) {
removeKeyListener(keyListeners[i]);
}
// Add new key listeners
addKeyListener(new KeyAdapter() {
private boolean armed = false;
@Override
public void keyPressed(KeyEvent e) {
int modifiers = e.getModifiers();
int keyCode = e.getKeyCode();
if ((modifiers > 0 && modifiers != KeyEvent.SHIFT_MASK) || e.isActionKey())
return ;
char c = e.getKeyChar();
if (!Character.isISOControl(c) && keyCode != KeyEvent.VK_SHIFT && keyCode != KeyEvent.VK_ESCAPE) {
armed = true;
e.consume();
}
}
@Override
public void keyTyped(KeyEvent e) {
if (armed) {
final KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e);
searchTextField.setText(String.valueOf(stroke.getKeyChar()));
displaySearchField();
e.consume();
armed = false;
}
}
});
// Create a the "multi-event" listener for the text field. Instead of
// adding separate instances of each needed listener, we're using a
// class which implements them all. This approach is used in order
// to avoid the creation of 4 instances which takes some time
SearchFieldListener searchFieldListener = new SearchFieldListener();
searchTextField.addKeyListener(searchFieldListener);
searchTextField.addFocusListener(searchFieldListener);
searchTextField.getDocument().addDocumentListener(searchFieldListener);
}
/**
* Listener showing and operating the search box.
*/
private class SearchFieldListener extends KeyAdapter
implements DocumentListener, FocusListener {
/** The last search results */
private List results = new ArrayList<>();
/** The last selected index from the search results. */
private int currentSelectionIndex;
/**
* Default constructor.
*/
SearchFieldListener() {
}
@Override
public void changedUpdate(DocumentEvent e) {
searchForRow();
}
@Override
public void insertUpdate(DocumentEvent e) {
searchForRow();
}
@Override
public void removeUpdate(DocumentEvent e) {
searchForRow();
}
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_ESCAPE) {
removeSearchField();
ETable.this.requestFocus();
} else if (keyCode == KeyEvent.VK_UP) {
currentSelectionIndex--;
displaySearchResult();
// Stop processing the event here. Otherwise it's dispatched
// to the table too (which scrolls)
e.consume();
} else if (keyCode == KeyEvent.VK_DOWN) {
currentSelectionIndex++;
displaySearchResult();
// Stop processing the event here. Otherwise it's dispatched
// to the table too (which scrolls)
e.consume();
} else if (keyCode == KeyEvent.VK_TAB) {
if (maxPrefix != null) {
searchTextField.setText(maxPrefix);
}
e.consume();
} else if (keyCode == KeyEvent.VK_ENTER) {
removeSearchField();
// TODO: do something on hitting enter???
e.consume();
ETable.this.requestFocus();
}
}
/** Searches for a row. */
private void searchForRow() {
currentSelectionIndex = 0;
results.clear();
maxPrefix = null;
String text = searchTextField.getText().toUpperCase();
if (text.length() > 0) {
results = doSearch(text);
// do search forward the selected index
int rows[] = getSelectedRows();
int selectedRowIndex = (rows == null || rows.length == 0) ? 0 : rows[0];
int r = 0;
for (Iterator it = results.iterator(); it.hasNext(); r++) {
int curResult = it.next().intValue();
if (selectedRowIndex <= curResult) {
currentSelectionIndex = r;
break;
}
}
displaySearchResult();
}
}
private void displaySearchResult() {
int sz = results.size();
if (sz > 0) {
if (currentSelectionIndex < 0) {
currentSelectionIndex = 0;
}
if (currentSelectionIndex >= sz) {
currentSelectionIndex = sz - 1;
}
int selRow = results.get(currentSelectionIndex).intValue();
setRowSelectionInterval(selRow, selRow);
Rectangle rect = getCellRect(selRow, 0, true);
scrollRectToVisible(rect);
displaySearchField();
} else {
clearSelection();
}
}
@Override
public void focusGained(FocusEvent e) {
// Do nothing
}
@Override
public void focusLost(FocusEvent e) {
Component c = e.getOppositeComponent();
if (c != searchCombo) {
removeSearchField();
}
}
}
/**
* Listener showing and operating the search box.
*/
private class SearchComboListener extends KeyAdapter
implements FocusListener, ItemListener {
/**
* Default constructor.
*/
SearchComboListener() {
}
@Override
public void itemStateChanged(java.awt.event.ItemEvent itemEvent) {
Object selItem = searchCombo.getSelectedItem();
for (Enumeration en = getColumnModel().getColumns(); en.hasMoreElements(); ) {
TableColumn column = en.nextElement();
if (column instanceof ETableColumn) {
ETableColumn etc = (ETableColumn)column;
Object value = etc.getHeaderValue();
String valueString = "";
if (value != null) {
valueString = value.toString();
}
valueString = getColumnDisplayName(valueString);
if (valueString.equals(selItem)) {
searchColumn = etc;
}
}
}
String text = searchTextField.getText();
searchTextField.setText("");
searchTextField.setText(text);
searchTextField.requestFocus();
}
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_ESCAPE) {
removeSearchField();
ETable.this.requestFocus();
}
}
@Override
public void focusGained(FocusEvent e) {
// Do nothing
}
@Override
public void focusLost(FocusEvent e) {
Component c = e.getOppositeComponent();
if (c != searchTextField) {
removeSearchField();
}
}
}
private void prepareSearchPanel() {
if (searchPanel == null) {
searchPanel = new JPanel();
String s = UIManager.getString("LBL_QUICKSEARCH");
if (s == null) {
s = "Quick search in";
}
JLabel lbl = new JLabel(s); //NOI18N
searchPanel.setLayout (
new BoxLayout(searchPanel, BoxLayout.X_AXIS));
searchPanel.add (lbl);
searchCombo = new JComboBox(getSearchComboModel());
if (searchColumn != null) {
Object value = searchColumn.getHeaderValue();
String valueString = "";
if (value != null) {
valueString = value.toString();
}
valueString = getColumnDisplayName(valueString);
searchCombo.setSelectedItem(valueString);
}
SearchComboListener scl = new SearchComboListener();
searchCombo.addItemListener(scl);
searchCombo.addFocusListener(scl);
searchCombo.addKeyListener(scl);
searchPanel.add(searchCombo);
searchPanel.add (searchTextField);
lbl.setLabelFor(searchTextField);
searchPanel.setBorder (BorderFactory.createRaisedBevelBorder());
lbl.setBorder (BorderFactory.createEmptyBorder (0, 0, 0, 5));
}
}
private ComboBoxModel getSearchComboModel() {
DefaultComboBoxModel result = new DefaultComboBoxModel<>();
for (Enumeration en = getColumnModel().getColumns(); en.hasMoreElements(); ) {
TableColumn column = en.nextElement();
if (column instanceof ETableColumn) {
ETableColumn etc = (ETableColumn)column;
Object value = etc.getHeaderValue();
String valueString = "";
if (value != null) {
valueString = value.toString();
}
valueString = getColumnDisplayName(valueString);
result.addElement(valueString);
}
}
return result;
}
/**
* Shows the search field.
*/
public void displaySearchField() {
if (!searchTextField.isDisplayable()) {
searchTextField.setFont(ETable.this.getFont());
prepareSearchPanel();
add(searchPanel);
}
doLayout();
invalidate();
validate();
repaint();
searchTextField.requestFocus();
}
/**
* Overridden to place the search text field.
* @see javax.swing.JTable#doLayout()
*/
@Override
public void doLayout() {
super.doLayout();
Rectangle visibleRect = getVisibleRect();
if (searchPanel != null && searchPanel.isDisplayable()) {
int width = Math.min (
visibleRect.width - SEARCH_FIELD_SPACE * 2,
searchPanel.getPreferredSize().width - searchTextField.getPreferredSize().width +
SEARCH_FIELD_PREFERRED_SIZE - SEARCH_FIELD_SPACE);
searchPanel.setBounds(
Math.max (SEARCH_FIELD_SPACE,
visibleRect.x + visibleRect.width - width),
visibleRect.y + SEARCH_FIELD_SPACE,
Math.min (visibleRect.width, width) - SEARCH_FIELD_SPACE,
heightOfTextField);
}
}
/**
* Removes the search field from the table.
*/
private void removeSearchField() {
if (searchPanel.isDisplayable()) {
remove(searchPanel);
Rectangle r = searchPanel.getBounds();
this.repaint(r);
}
}
/**
* Item to the collection when doing the sorting of table rows.
*/
public static final class RowMapping {
// index (of the row) in the TableModel
private final int originalIndex;
// table model of my table
private final TableModel model;
// The table
private final ETable table;
// The cached transformed value
private Object transformed = TRANSFORMED_NONE;
// The column of the transformed value
private int transformedColumn;
// When values from more columns are requested, the transformed values
// are stored in this array
private Object[] allTransformed = null;
private static final Object TRANSFORMED_NONE = new Object();
/**
* Create a new table row mapping.
* @param index The row index
* @param model The table model
*/
public RowMapping(int index, TableModel model) {
originalIndex = index;
this.model = model;
this.table = null;
}
/**
* Create a new table row mapping.
* @param index The row index
* @param model The table model
* @param table The table. This argument needs to be set when
* {@link #getTransformedValue(int)} is to be used.
* @since 1.19
*/
public RowMapping(int index, TableModel model, ETable table) {
originalIndex = index;
this.model = model;
this.table = table;
}
/**
* Get the model row index.
* @return The model row index
*/
public int getModelRowIndex() {
return originalIndex;
}
/**
* Get the model object at the row index of this mapping and the given column.
* @param column The column
* @return The model object
* @see #getTransformedValue(int)
*/
public Object getModelObject(int column) {
return model.getValueAt(originalIndex, column);
}
/**
* Get the transformed value. This method assures that we retrieve every
* value only once per any number of calls. The table needs to be set
* in order to be able to transform the value, see
* {@link #RowMapping(int, javax.swing.table.TableModel, org.netbeans.swing.etable.ETable)}.
*
* @param column The column
* @return The transformed value. It returns the cached value of
* table.transformValue(getModelObject(column))
.
* @throws IllegalStateException when table is not set
* @since 1.19
*/
public Object getTransformedValue(int column) {
if (column >= model.getColumnCount()) { // #239045
return null;
}
if (table == null) {
throw new IllegalStateException("The table was not set.");
}
Object value;
if (TRANSFORMED_NONE == transformed) {
value = transformed = table.transformValue(getModelObject(column));
transformedColumn = column;
} else if (allTransformed != null) {
if (allTransformed.length <= column) {
Object[] newTransformed = new Object[column + 1];
System.arraycopy(allTransformed, 0, newTransformed, 0, allTransformed.length);
for (int i = allTransformed.length; i < newTransformed.length; i++) {
newTransformed[i] = TRANSFORMED_NONE;
}
allTransformed = newTransformed;
}
if (TRANSFORMED_NONE == allTransformed[column]) {
value = allTransformed[column] = table.transformValue(getModelObject(column));
} else {
value = allTransformed[column];
}
} else if (transformedColumn != column) {
int n = Math.max(transformedColumn, column) + 1;
allTransformed = new Object[n];
for (int i = 0; i < n; i++) {
allTransformed[i] = TRANSFORMED_NONE;
}
allTransformed[transformedColumn] = transformed;
value = allTransformed[column] = table.transformValue(getModelObject(column));
} else {
value = transformed;
}
return value;
}
}
/**
* Comparator for RowMapping objects that sorts according
* to the original indices of the rows in the model.
*/
static class OriginalRowComparator implements Comparator {
public OriginalRowComparator() {
}
@Override
public int compare(RowMapping rm1, RowMapping rm2) {
int i1 = rm1.getModelRowIndex();
int i2 = rm2.getModelRowIndex();
return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1));
}
}
private void showColumnSelection(MouseEvent me) {
ColumnSelection cs = getColumnSelectionOn(me.getButton());
switch (cs) {
case POPUP:
ColumnSelectionPanel.showColumnSelectionPopup (me.getComponent (), me.getX(), me.getY(), ETable.this);
break;
case DIALOG:
ColumnSelectionPanel.showColumnSelectionDialog(ETable.this);
break;
}
}
/**
* Mouse listener attached to the scroll pane of this table, handles the case
* when no columns are displayed.
*/
private class ColumnSelectionMouseListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent me) {
if (me.getButton() != MouseEvent.BUTTON1) {
showColumnSelection(me);
}
}
}
/**
* Mouse listener attached to the JTableHeader of this table. Single
* click on the table header should trigger sorting on that column.
* Double click on the column divider automatically resizes the column.
*/
private class HeaderMouseListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent me) {
if (me.getButton() == MouseEvent.BUTTON3) { // Other buttons are reserved for sorting
showColumnSelection(me);
return;
}
TableColumn resColumn = getResizingColumn(me.getPoint());
if (sortable && (resColumn == null) && (me.getClickCount() == 1)) {
// ok, do the sorting
int column = columnAtPoint(me.getPoint());
if (column < 0) return ;
TableColumnModel tcm = getColumnModel();
if (tcm instanceof ETableColumnModel) {
ETableColumnModel etcm = (ETableColumnModel)tcm;
TableColumn tc = tcm.getColumn(column);
if (tc instanceof ETableColumn) {
ETableColumn etc = (ETableColumn)tc;
if (! etc.isSortingAllowed()) {
return;
}
SelectedRows selectedRows;
int wasSelectedColumn;
if (getUpdateSelectionOnSort()) {
selectedRows = getSelectedRowsInModel();
wasSelectedColumn = getSelectedColumn();
} else {
selectedRows = null;
wasSelectedColumn = -1;
}
boolean clear = ((me.getModifiers() & InputEvent.SHIFT_MASK) != InputEvent.SHIFT_MASK);
etcm.toggleSortedColumn(etc, clear);
resetPermutation ();
ETable.super.tableChanged(new TableModelEvent(getModel(), 0, getRowCount() - 1));
if (selectedRows != null) {
changeSelectionInModel(selectedRows, wasSelectedColumn);
}
getTableHeader().resizeAndRepaint();
}
}
}
if ((resColumn != null) && (me.getClickCount() == 2)) {
// update the column width
if (resColumn instanceof ETableColumn) {
ETableColumn etc = (ETableColumn)resColumn;
etc.updatePreferredWidth(ETable.this, true);
}
}
}
}
/**
* Overridden to force requesting the focus after the user starts editing.
* @see javax.swing.JTable#editCellAt(int, int, EventObject)
*/
@Override
public boolean editCellAt(int row, int column, EventObject e) {
inEditRequest = true;
if (editingRow == row && editingColumn == column && isEditing()) {
//discard edit requests if we're already editing that cell
inEditRequest = false;
return false;
}
if (isEditing()) {
removeEditor();
changeSelection(row, column, false, false);
}
try {
boolean ret = super.editCellAt(row, column, e);
if (ret) {
editorComp.requestFocus();
}
return ret;
} finally {
inEditRequest = false;
}
}
/**
* Overridden to track whether the remove request is in progress.
* @see javax.swing.JTable#removeEditor()
*/
@Override
public void removeEditor() {
inRemoveRequest = true;
try {
synchronized (getTreeLock()) {
super.removeEditor();
}
} finally {
inRemoveRequest = false;
}
}
/**
* Checks whether the given component is "our".
*/
private boolean isKnownComponent(Component c) {
if (c == null) return false;
if (isAncestorOf (c)) {
return true;
}
if (c == editorComp) {
return true;
}
if (editorComp instanceof Container &&
((Container) editorComp).isAncestorOf(c)) {
return true;
}
return false;
}
/**
* Focus transfer policy that retains focus after closing an editor.
* Copied wholesale from org.openide.explorer.view.TreeTable.
*/
private class STPolicy extends ContainerOrderFocusTraversalPolicy {
@Override
public Component getComponentAfter(Container focusCycleRoot,
Component aComponent) {
if (inRemoveRequest) {
return ETable.this;
} else {
Component result = super.getComponentAfter(focusCycleRoot, aComponent);
return result;
}
}
@Override
public Component getComponentBefore(Container focusCycleRoot,
Component aComponent) {
if (inRemoveRequest) {
return ETable.this;
} else {
return super.getComponentBefore(focusCycleRoot, aComponent);
}
}
@Override
public Component getFirstComponent(Container focusCycleRoot) {
if (!inRemoveRequest && isEditing()) {
return editorComp;
} else {
return ETable.this;
}
}
@Override
public Component getDefaultComponent(Container focusCycleRoot) {
if (inRemoveRequest && isEditing() && editorComp.isShowing()) {
return editorComp;
} else {
return ETable.this;
}
}
@Override
protected boolean accept(Component aComponent) {
//Do not allow focus to go to a child of the editor we're using if
//we are in the process of removing the editor
if (isEditing() && inEditRequest) {
return isKnownComponent (aComponent);
}
return super.accept(aComponent) && aComponent.isShowing();
}
}
/**
* Enables tab keys to navigate between rows but also exit the table
* to the next focusable component in either direction.
*/
private final class NavigationAction extends AbstractAction {
/** true is forward direction */
private boolean direction;
public NavigationAction(boolean direction) {
this.direction = direction;
}
@Override
public void actionPerformed(ActionEvent e) {
if (isEditing()) {
removeEditor();
}
int targetRow;
int targetColumn;
if (direction) {
if (getSelectedColumn() == getColumnCount()-1) {
targetColumn=0;
targetRow = getSelectedRow()+1;
} else {
targetColumn = getSelectedColumn()+1;
targetRow = getSelectedRow();
}
} else {
if (getSelectedColumn() == 0) {
targetColumn = getColumnCount()-1;
targetRow = getSelectedRow()-1;
} else {
targetRow = getSelectedRow();
targetColumn = getSelectedColumn() -1;
}
}
//if we're off the end, try to find a sibling component to pass
//focus to
if (targetRow >= getRowCount() || targetRow < 0) {
//This code is a bit ugly, but works
Container ancestor = getFocusCycleRootAncestor();
//Find the next component in our parent's focus cycle
Component sibling = direction ?
ancestor.getFocusTraversalPolicy().getComponentAfter(ancestor,
ETable.this) :
ancestor.getFocusTraversalPolicy().getComponentBefore(ancestor,
ETable.this);
//Often LayoutFocusTranferPolicy will return ourselves if we're
//the last. First try to find a parent focus cycle root that
//will be a little more polite
if (sibling == ETable.this) {
Container grandcestor = ancestor.getFocusCycleRootAncestor();
if (grandcestor != null) {
sibling = direction ? grandcestor.getFocusTraversalPolicy().getComponentAfter(grandcestor, ancestor) :
grandcestor.getFocusTraversalPolicy().getComponentBefore(grandcestor, ancestor);
ancestor = grandcestor;
}
}
//Okay, we still ended up with ourselves, or there is only one focus
//cycle root ancestor. Try to find the first component according to
//the policy
if (sibling == ETable.this) {
if (ancestor.getFocusTraversalPolicy().getFirstComponent(ancestor) != null) {
sibling = ancestor.getFocusTraversalPolicy().getFirstComponent(ancestor);
}
}
//If we're *still* getting ourselves, find the default button and punt
if (sibling == ETable.this) {
JRootPane rp = getRootPane();
JButton jb = rp.getDefaultButton();
if (jb != null) {
sibling = jb;
}
}
//See if it's us, or something we know about, and if so, just
//loop around to the top or bottom row - there's noplace
//interesting for focus to go to
if (sibling != null) {
if (sibling == ETable.this) {
//set the selection if there's nothing else to do
changeSelection(direction ? 0 : getRowCount()-1,
direction ? 0 : getColumnCount()-1,false,false);
} else {
//Request focus on the sibling
sibling.requestFocus();
}
return;
}
}
changeSelection (targetRow, targetColumn, false, false);
}
}
/** Used to explicitly invoke editing from the keyboard */
private class EditAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
int row = getSelectedRow();
int col = getSelectedColumn();
int[] selectedRows = getSelectedRows();
boolean edited = editCellAt(row, col, e);
if (!edited) {
for (int r : selectedRows) {
if (r != row) {
edited = editCellAt(r, col, e);
if (edited) {
break;
}
}
}
}
}
@Override
public boolean isEnabled() {
return getSelectedRow() != -1 && getSelectedColumn() != -1 && !isEditing();
}
}
/**
* Either cancels an edit, or closes the enclosing dialog if present.
*/
private class CancelEditAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
if (isEditing() || editorComp != null) {
removeEditor();
return;
} else {
Component c = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
InputMap imp = getRootPane().getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
ActionMap am = getRootPane().getActionMap();
KeyStroke escape = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
Object key = imp.get(escape);
if (key == null) {
//Default for NbDialog
key = "Cancel";
}
if (key != null) {
Action a = am.get(key);
if (a != null) {
String commandKey = (String)a.getValue(Action.ACTION_COMMAND_KEY);
if (commandKey == null) {
commandKey = key.toString();
}
a.actionPerformed(new ActionEvent(this,
ActionEvent.ACTION_PERFORMED, commandKey)); //NOI18N
}
}
}
}
@Override
public boolean isEnabled() {
// return isEditing();
return true;
}
}
/**
* Action for the keyboard event of hitting the Enter key.
*/
private class EnterAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
JRootPane jrp = getRootPane();
if (jrp != null) {
JButton b = getRootPane().getDefaultButton();
if (b != null && b.isEnabled()) {
b.doClick();
}
}
}
@Override
public boolean isEnabled() {
return !isEditing() && !inRemoveRequest;
}
}
/**
* CTRL-Tab transfers focus up.
*/
private class CTRLTabAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
setFocusCycleRoot(false);
try {
Container con = ETable.this.getFocusCycleRootAncestor();
if (con != null) {
/*
Component target = ETable.this;
if (getParent() instanceof JViewport) {
target = getParent().getParent();
if (target == con) {
target = ETable.this;
}
}
*/
EventObject eo = EventQueue.getCurrentEvent();
boolean backward = false;
if (eo instanceof KeyEvent) {
backward =
(((KeyEvent) eo).getModifiers()
& KeyEvent.SHIFT_MASK)
!= 0 && (((KeyEvent) eo).getModifiersEx() &
KeyEvent.SHIFT_DOWN_MASK) != 0;
}
Component c = ETable.this;
Component to;
Container parentWithFTP = null;
do {
FocusTraversalPolicy ftp = con.getFocusTraversalPolicy();
to = backward ? ftp.getComponentBefore(con, c)
: ftp.getComponentAfter(con, c);
if (to == ETable.this) {
to = backward ? ftp.getFirstComponent(con)
: ftp.getLastComponent(con);
}
if (to == ETable.this) {
parentWithFTP = con.getParent();
if (parentWithFTP != null) {
parentWithFTP = parentWithFTP.getFocusCycleRootAncestor();
}
if (parentWithFTP != null) {
c = con;
con = parentWithFTP;
}
}
} while (to == ETable.this && parentWithFTP != null);
if (to != null) {
to.requestFocus();
}
}
} finally {
setFocusCycleRoot(true);
}
}
}
/**
* Allows to supply alternative implementation of the column
* selection functionality in ETable. If columnSelector is null
* the defaultColumnSelector is returned by this method.
*/
public TableColumnSelector getColumnSelector() {
return columnSelector != null ? columnSelector : defaultColumnSelector;
}
/**
* Allows to supply alternative implementation of the column
* selection functionality in ETable.
*/
public void setColumnSelector(TableColumnSelector columnSelector) {
this.columnSelector = columnSelector;
}
/**
* The column selection corner can use either dialog or popup menu.
*
* @return true
, when left mouse click invokes a popup menu,
* or false
, when left mouse click opens a dialog for column selection.
*/
public boolean isPopupUsedFromTheCorner() {
synchronized (columnSelectionOnMouseClickLock) {
ColumnSelection cs = columnSelectionOnMouseClick[1];
return cs == ColumnSelection.POPUP;
}
}
/**
* The column selection corner can use either dialog or popup menu.
* This method is equivalent to {@link #setColumnSelectionOn(int, org.netbeans.swing.etable.ETable.ColumnSelection)}
* with arguments 1
and appropriate column selection constant.
*
* @param popupUsedFromTheCorner When true
, left mouse click invokes a popup menu,
* when false
, left mouse click opens a dialog for column selection.
*/
public void setPopupUsedFromTheCorner(boolean popupUsedFromTheCorner) {
synchronized (columnSelectionOnMouseClickLock) {
columnSelectionOnMouseClick[1] = popupUsedFromTheCorner ? ColumnSelection.POPUP : ColumnSelection.DIALOG;
}
}
/**
* Get the column selection method, that is displayed as a response to the
* mouse event. A popup with column selection menu, or column selection
* dialog can be displayed.
* By default, popup menu is displayed on button3 mouse click
* and dialog or popup menu is displayed on the corner
* button1 mouse action, depending on the value of {@link #isPopupUsedFromTheCorner()}
*
* @param mouseButton The button of the mouse event
* @return The column selection method.
* @since 1.17
*/
public ColumnSelection getColumnSelectionOn(int mouseButton) {
if (mouseButton < 0) {
throw new IllegalArgumentException("Button = "+mouseButton);
}
synchronized (columnSelectionOnMouseClickLock) {
if (mouseButton >= columnSelectionOnMouseClick.length) {
return null;
}
return columnSelectionOnMouseClick[mouseButton];
}
}
/**
* Set if popup with column selection menu or column selection dialog
* should be displayed as a response to the mouse event.
*
* @param mouseButton The button of the mouse event
* @param selection The column selection method.
* @since 1.17
*/
public void setColumnSelectionOn(int mouseButton, ColumnSelection selection) {
if (mouseButton < 0) {
throw new IllegalArgumentException("Button = "+mouseButton);
}
synchronized (columnSelectionOnMouseClickLock) {
if (mouseButton >= columnSelectionOnMouseClick.length) {
ColumnSelection[] csp = new ColumnSelection[mouseButton + 1];
System.arraycopy(columnSelectionOnMouseClick, 0, csp, 0, columnSelectionOnMouseClick.length);
columnSelectionOnMouseClick = csp;
}
columnSelectionOnMouseClick[mouseButton] = selection;
}
}
/**
* Shows dialog that allows to show/hide columns.
* @since 1.17
*/
public final void showColumnSelectionDialog() {
ColumnSelectionPanel.showColumnSelectionDialog(this);
}
/**
* Default column selector is used when columnSelector is null.
*/
public static TableColumnSelector getDefaultColumnSelector() {
return defaultColumnSelector;
}
/**
* Allows to supply the default column selector for all instances
* of ETable.
*/
public static void setDefaultColumnSelector(TableColumnSelector aDefaultColumnSelector) {
defaultColumnSelector = aDefaultColumnSelector;
}
private static final class SelectedRows {
int anchorInView;
int anchorInModel;
int leadInView;
int leadInModel;
int[] rowsInView;
int[] rowsInModel;
@Override
public String toString() {
return "SelectedRows[anchorInView="+anchorInView+", anchorInModel="+anchorInModel+
", leadInView="+leadInView+", leadInModel="+leadInModel+
", rowsInView="+Arrays.toString(rowsInView)+", rowsInModel="+Arrays.toString(rowsInModel)+"]";
}
}
// JDK 6 methods added to JTable:
@Override
public void setAutoCreateRowSorter(boolean autoCreateRowSorter) {
if (getColumnModel() instanceof ETableColumnModel) {
if (autoCreateRowSorter) {
throw new UnsupportedOperationException("ETable with ETableColumnModel has it's own sorting mechanism. JTable's RowSorter can not be used.");
}
}
super.setAutoCreateRowSorter(autoCreateRowSorter);
}
/* Added and not needed to be overidden.
@Override
public void setUpdateSelectionOnSort(boolean update) {
super.setUpdateSelectionOnSort(update);
}
@Override
public boolean getUpdateSelectionOnSort() {
return super.getUpdateSelectionOnSort();
}
*/
/**
* When ETable has ETableColumnModel set, only null
sorter
* is accepted, which turns off sorting. Otherwise UnsupportedOperationException is thrown.
* RowSorter can be used when a different TableColumnModel is set.
*
* @param sorter {@inheritDoc}
*/
@Override
public void setRowSorter(RowSorter extends TableModel> sorter) {
if (getColumnModel() instanceof ETableColumnModel) {
if (sorter == null) {
sortable = false;
((ETableColumnModel) getColumnModel()).clearSortedColumns();
} else {
throw new UnsupportedOperationException(
"ETable with ETableColumnModel has it's own sorting mechanism. Use ETableColumnModel to define sorting, or set a different TableColumnModel.");
}
} else {
super.setRowSorter(sorter);
}
}
/**
* Get the RowSorter in case that the ETable does not have ETableColumnModel set.
* @return {@inheritDoc}
*/
@Override
public RowSorter extends TableModel> getRowSorter() {
if (getColumnModel() instanceof ETableColumnModel) {
return null;
} else {
return super.getRowSorter();
}
}
}