blob: a65c209399bffa8619fc881f593269b2fa946b56 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed 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 com.intellij.ui.table;
import com.intellij.Patches;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.*;
import com.intellij.ui.components.JBViewport;
import com.intellij.ui.speedSearch.SpeedSearchSupply;
import com.intellij.util.ui.*;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;
import java.util.Comparator;
import java.util.EventObject;
public class JBTable extends JTable implements ComponentWithEmptyText, ComponentWithExpandableItems<TableCell> {
public static final int PREFERRED_SCROLLABLE_VIEWPORT_HEIGHT_IN_ROWS = 7;
private final StatusText myEmptyText;
private final ExpandableItemsHandler<TableCell> myExpandableItemsHandler;
private MyCellEditorRemover myEditorRemover;
private boolean myEnableAntialiasing;
private int myRowHeight = -1;
private boolean myRowHeightIsExplicitlySet;
private boolean myRowHeightIsComputing;
private Integer myMinRowHeight;
private boolean myStriped;
private AsyncProcessIcon myBusyIcon;
private boolean myBusy;
public JBTable() {
this(new DefaultTableModel());
}
public JBTable(TableModel model) {
this(model, null);
}
public JBTable(final TableModel model, final TableColumnModel columnModel) {
super(model, columnModel);
setSurrendersFocusOnKeystroke(true);
myEmptyText = new StatusText(this) {
@Override
protected boolean isStatusVisible() {
return isEmpty();
}
};
myExpandableItemsHandler = ExpandableItemsHandlerFactory.install(this);
setFillsViewportHeight(true);
addMouseListener(new MyMouseListener());
getColumnModel().addColumnModelListener(new TableColumnModelListener() {
@Override
public void columnMarginChanged(ChangeEvent e) {
if (cellEditor != null && !(cellEditor instanceof Animated)) {
cellEditor.stopCellEditing();
}
}
@Override
public void columnSelectionChanged(@NotNull ListSelectionEvent e) {
}
@Override
public void columnAdded(@NotNull TableColumnModelEvent e) {
}
@Override
public void columnMoved(@NotNull TableColumnModelEvent e) {
}
@Override
public void columnRemoved(@NotNull TableColumnModelEvent e) {
}
});
final TableModelListener modelListener = new TableModelListener() {
@Override
public void tableChanged(@NotNull final TableModelEvent e) {
if (!myRowHeightIsExplicitlySet) {
myRowHeight = -1;
}
if (e.getType() == TableModelEvent.DELETE && isEmpty() || e.getType() == TableModelEvent.INSERT && !isEmpty()) {
repaintViewport();
}
}
};
if (getModel() != null) getModel().addTableModelListener(modelListener);
addPropertyChangeListener("model", new PropertyChangeListener() {
@Override
public void propertyChange(@NotNull PropertyChangeEvent evt) {
repaintViewport();
if (evt.getOldValue() instanceof TableModel) {
((TableModel)evt.getOldValue()).removeTableModelListener(modelListener);
}
if (evt.getNewValue() instanceof TableModel) {
((TableModel)evt.getNewValue()).addTableModelListener(modelListener);
}
}
});
//noinspection UnusedDeclaration
boolean marker = Patches.SUN_BUG_ID_4503845; // Don't remove. It's a marker for find usages
}
@Override
public int getRowHeight() {
if (myRowHeightIsComputing) {
return super.getRowHeight();
}
if (myRowHeight < 0) {
try {
myRowHeightIsComputing = true;
for (int row = 0; row < getRowCount(); row++) {
for (int column = 0; column < getColumnCount(); column++) {
final TableCellRenderer renderer = getCellRenderer(row, column);
if (renderer != null) {
final Object value = getValueAt(row, column);
final Component component = renderer.getTableCellRendererComponent(this, value, true, true, row, column);
if (component != null) {
final Dimension size = component.getPreferredSize();
myRowHeight = Math.max(size.height, myRowHeight);
}
}
}
}
}
finally {
myRowHeightIsComputing = false;
}
}
if (myMinRowHeight == null) {
myMinRowHeight = getFontMetrics(UIManager.getFont("Label.font")).getHeight();
}
return Math.max(myRowHeight, myMinRowHeight);
}
public void setShowColumns(boolean value) {
JTableHeader tableHeader = getTableHeader();
tableHeader.setVisible(value);
tableHeader.setPreferredSize(value ? null : new Dimension());
}
@Override
public void setRowHeight(int rowHeight) {
myRowHeight = rowHeight;
myRowHeightIsExplicitlySet = true;
// call super to clean rowModel
super.setRowHeight(rowHeight);
}
@Override
public void updateUI() {
super.updateUI();
myMinRowHeight = null;
}
private void repaintViewport() {
if (!isDisplayable() || !isVisible()) return;
Container p = getParent();
if (p instanceof JBViewport) {
p.repaint();
}
}
@NotNull
@Override
protected JTableHeader createDefaultTableHeader() {
return new JBTableHeader();
}
public boolean isEmpty() {
return getRowCount() == 0;
}
@Override
public void setModel(@NotNull TableModel model) {
super.setModel(model);
if (model instanceof SortableColumnModel) {
final SortableColumnModel sortableModel = (SortableColumnModel)model;
if (sortableModel.isSortable()) {
final TableRowSorter<TableModel> rowSorter = createRowSorter(model);
rowSorter.setSortsOnUpdates(isSortOnUpdates());
setRowSorter(rowSorter);
final RowSorter.SortKey sortKey = sortableModel.getDefaultSortKey();
if (sortKey != null && sortKey.getColumn() >= 0 && sortKey.getColumn() < model.getColumnCount()) {
if (sortableModel.getColumnInfos()[sortKey.getColumn()].isSortable()) {
rowSorter.setSortKeys(Arrays.asList(sortKey));
}
}
}
else {
final RowSorter<? extends TableModel> rowSorter = getRowSorter();
if (rowSorter instanceof DefaultColumnInfoBasedRowSorter) {
setRowSorter(null);
}
}
}
}
protected boolean isSortOnUpdates() {
return true;
}
@Override
protected void paintComponent(@NotNull Graphics g) {
if (myEnableAntialiasing) {
GraphicsUtil.setupAntialiasing(g);
}
super.paintComponent(g);
myEmptyText.paint(this, g);
}
@Override
protected void paintChildren(Graphics g) {
if (myEnableAntialiasing) {
GraphicsUtil.setupAntialiasing(g);
}
super.paintChildren(g);
}
public void setEnableAntialiasing(boolean flag) {
myEnableAntialiasing = flag;
}
public static DefaultCellEditor createBooleanEditor() {
return new DefaultCellEditor(new JCheckBox()) {
{
((JCheckBox)getComponent()).setHorizontalAlignment(SwingConstants.CENTER);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
Component component = super.getTableCellEditorComponent(table, value, isSelected, row, column);
component.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground());
return component;
}
};
}
public void resetDefaultFocusTraversalKeys() {
KeyboardFocusManager m = KeyboardFocusManager.getCurrentKeyboardFocusManager();
for (Integer each : Arrays.asList(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS,
KeyboardFocusManager.DOWN_CYCLE_TRAVERSAL_KEYS)) {
setFocusTraversalKeys(each, m.getDefaultFocusTraversalKeys(each));
}
}
@NotNull
@Override
public StatusText getEmptyText() {
return myEmptyText;
}
@Override
@NotNull
public ExpandableItemsHandler<TableCell> getExpandableItemsHandler() {
return myExpandableItemsHandler;
}
@Override
public void setExpandableItemsEnabled(boolean enabled) {
myExpandableItemsHandler.setEnabled(enabled);
}
@Override
public void removeNotify() {
if (ScreenUtil.isStandardAddRemoveNotify(this)) {
final KeyboardFocusManager keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
//noinspection HardCodedStringLiteral
keyboardFocusManager.removePropertyChangeListener("permanentFocusOwner", myEditorRemover);
//noinspection HardCodedStringLiteral
keyboardFocusManager.removePropertyChangeListener("focusOwner", myEditorRemover);
super.removeNotify();
if (myBusyIcon != null) {
remove(myBusyIcon);
Disposer.dispose(myBusyIcon);
myBusyIcon = null;
}
}
else {
super.removeNotify();
}
}
@Override
public int getScrollableUnitIncrement(@NotNull Rectangle visibleRect, int orientation, int direction) {
if (orientation == SwingConstants.VERTICAL) {
return super.getScrollableUnitIncrement(visibleRect, orientation, direction);
}
else { // if orientation == SwingConstants.HORIZONTAL
// use smooth editor-like scrolling
return SwingUtilities.computeStringWidth(getFontMetrics(getFont()), " ");
}
}
@Override
public void doLayout() {
super.doLayout();
if (myBusyIcon != null) {
myBusyIcon.updateLocation(this);
}
}
@Override
public void paint(@NotNull Graphics g) {
if (!isEnabled()) {
g = new TableGrayer((Graphics2D)g);
}
super.paint(g);
if (myBusyIcon != null) {
myBusyIcon.updateLocation(this);
}
}
public void setPaintBusy(boolean paintBusy) {
if (myBusy == paintBusy) return;
myBusy = paintBusy;
updateBusy();
}
private void updateBusy() {
if (myBusy) {
if (myBusyIcon == null) {
myBusyIcon = new AsyncProcessIcon(toString()).setUseMask(false);
myBusyIcon.setOpaque(false);
myBusyIcon.setPaintPassiveIcon(false);
add(myBusyIcon);
}
}
if (myBusyIcon != null) {
if (myBusy) {
myBusyIcon.resume();
}
else {
myBusyIcon.suspend();
//noinspection SSBasedInspection
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (myBusyIcon != null) {
repaint();
}
}
});
}
if (myBusyIcon != null) {
myBusyIcon.updateLocation(this);
}
}
}
public boolean isStriped() {
return myStriped;
}
public void setStriped(boolean striped) {
myStriped = striped;
if (striped) {
getColumnModel().setColumnMargin(0);
setIntercellSpacing(new Dimension(getIntercellSpacing().width, 0));
setShowGrid(false);
}
}
@Override
public boolean editCellAt(final int row, final int column, final EventObject e) {
if (cellEditor != null && !cellEditor.stopCellEditing()) {
return false;
}
if (row < 0 || row >= getRowCount() || column < 0 || column >= getColumnCount()) {
return false;
}
if (!isCellEditable(row, column)) {
return false;
}
if (e instanceof KeyEvent) {
// do not start editing in autoStartsEdit mode on Ctrl-Z and other non-typed events
if (!UIUtil.isReallyTypedEvent((KeyEvent)e) || ((KeyEvent)e).getKeyChar() == KeyEvent.CHAR_UNDEFINED) return false;
SpeedSearchSupply supply = SpeedSearchSupply.getSupply(this);
if (supply != null && supply.isPopupActive()) {
return false;
}
}
if (myEditorRemover == null) {
final KeyboardFocusManager keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
myEditorRemover = new MyCellEditorRemover();
//noinspection HardCodedStringLiteral
keyboardFocusManager.addPropertyChangeListener("focusOwner", myEditorRemover);
//noinspection HardCodedStringLiteral
keyboardFocusManager.addPropertyChangeListener("permanentFocusOwner", myEditorRemover);
}
final TableCellEditor editor = getCellEditor(row, column);
if (editor != null && editor.isCellEditable(e)) {
editorComp = prepareEditor(editor, row, column);
//((JComponent)editorComp).setBorder(null);
if (editorComp == null) {
removeEditor();
return false;
}
editorComp.setBounds(getCellRect(row, column, false));
add(editorComp);
editorComp.validate();
if (surrendersFocusOnKeyStroke()) {
// this replaces focus request in JTable.processKeyBinding
final IdeFocusManager focusManager = IdeFocusManager.findInstanceByComponent(this);
focusManager.setTypeaheadEnabled(false);
focusManager.requestFocus(editorComp, true).doWhenProcessed(new Runnable() {
@Override
public void run() {
focusManager.setTypeaheadEnabled(true);
}
});
}
setCellEditor(editor);
setEditingRow(row);
setEditingColumn(column);
editor.addCellEditorListener(this);
return true;
}
return false;
}
/**
* Always returns false.
* If you're interested in value of JTable.surrendersFocusOnKeystroke property, call JBTable.surrendersFocusOnKeyStroke()
* @return false
* @see #surrendersFocusOnKeyStroke
*/
@Override
public boolean getSurrendersFocusOnKeystroke() {
return false; // prevents JTable.processKeyBinding from requesting editor component to be focused
}
public boolean surrendersFocusOnKeyStroke() {
return super.getSurrendersFocusOnKeystroke();
}
private static boolean isTableDecorationSupported() {
return UIUtil.isUnderAlloyLookAndFeel()
|| UIUtil.isUnderNativeMacLookAndFeel()
|| UIUtil.isUnderDarcula()
|| UIUtil.isUnderIntelliJLaF()
|| UIUtil.isUnderNimbusLookAndFeel()
|| UIUtil.isUnderWindowsLookAndFeel();
}
@NotNull
@Override
public Component prepareRenderer(@NotNull TableCellRenderer renderer, int row, int column) {
Component result = super.prepareRenderer(renderer, row, column);
// Fix GTK background
if (UIUtil.isUnderGTKLookAndFeel()) {
UIUtil.changeBackGround(this, UIUtil.getTreeTextBackground());
}
if (isTableDecorationSupported() && isStriped() && result instanceof JComponent) {
final Color bg = row % 2 == 1 ? getBackground() : UIUtil.getDecoratedRowColor();
final JComponent c = (JComponent)result;
final boolean cellSelected = isCellSelected(row, column);
if (!cellSelected) {
c.setOpaque(true);
c.setBackground(bg);
for (Component child : c.getComponents()) {
child.setBackground(bg);
}
}
}
if (myExpandableItemsHandler.getExpandedItems().contains(new TableCell(row, column))) {
result = new ExpandedItemRendererComponentWrapper(result);
}
return result;
}
private final class MyCellEditorRemover implements PropertyChangeListener {
private final IdeFocusManager myFocusManager;
public MyCellEditorRemover() {
myFocusManager = IdeFocusManager.findInstanceByComponent(JBTable.this);
}
@Override
public void propertyChange(@NotNull final PropertyChangeEvent e) {
if (!isEditing()) {
return;
}
myFocusManager.doWhenFocusSettlesDown(new Runnable() {
@Override
public void run() {
if (!isEditing()) {
return;
}
Component c = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
while (c != null) {
if (c instanceof JPopupMenu) {
c = ((JPopupMenu)c).getInvoker();
}
if (c == JBTable.this) {
// focus remains inside the table
return;
}
else if (c instanceof Window) {
if (c == SwingUtilities.getWindowAncestor(JBTable.this)) {
getCellEditor().stopCellEditing();
}
break;
}
c = c.getParent();
}
}
});
}
}
private final class MyMouseListener extends MouseAdapter {
@Override
public void mousePressed(@NotNull final MouseEvent e) {
if (JBSwingUtilities.isRightMouseButton(e)) {
final int[] selectedRows = getSelectedRows();
if (selectedRows.length < 2) {
final int row = rowAtPoint(e.getPoint());
if (row != -1) {
getSelectionModel().setSelectionInterval(row, row);
}
}
}
}
}
@SuppressWarnings({"MethodMayBeStatic", "unchecked"})
protected TableRowSorter<TableModel> createRowSorter(final TableModel model) {
return new DefaultColumnInfoBasedRowSorter(model);
}
protected static class DefaultColumnInfoBasedRowSorter extends TableRowSorter<TableModel> {
public DefaultColumnInfoBasedRowSorter(final TableModel model) {
super(model);
setModelWrapper(new TableRowSorterModelWrapper(model));
setMaxSortKeys(1);
}
@Override
public Comparator<?> getComparator(final int column) {
final TableModel model = getModel();
if (model instanceof SortableColumnModel) {
final ColumnInfo[] columnInfos = ((SortableColumnModel)model).getColumnInfos();
if (column >= 0 && column < columnInfos.length) {
final Comparator comparator = columnInfos[column].getComparator();
if (comparator != null) return comparator;
}
}
return super.getComparator(column);
}
@Override
protected boolean useToString(int column) {
return false;
}
@Override
public boolean isSortable(final int column) {
final TableModel model = getModel();
if (model instanceof SortableColumnModel) {
final ColumnInfo[] columnInfos = ((SortableColumnModel)model).getColumnInfos();
if (column >= 0 && column < columnInfos.length) {
return columnInfos[column].isSortable() && columnInfos[column].getComparator() != null;
}
}
return false;
}
private class TableRowSorterModelWrapper extends ModelWrapper<TableModel, Integer> {
private final TableModel myModel;
private TableRowSorterModelWrapper(@NotNull TableModel model) {
myModel = model;
}
@Override
public TableModel getModel() {
return myModel;
}
@Override
public int getColumnCount() {
return myModel.getColumnCount();
}
@Override
public int getRowCount() {
return myModel.getRowCount();
}
@Override
public Object getValueAt(int row, int column) {
if (myModel instanceof SortableColumnModel) {
return ((SortableColumnModel)myModel).getRowValue(row);
}
return myModel.getValueAt(row, column);
}
@NotNull
@Override
public String getStringValueAt(int row, int column) {
TableStringConverter converter = getStringConverter();
if (converter != null) {
// Use the converter
String value = converter.toString(
myModel, row, column);
if (value != null) {
return value;
}
return "";
}
// No converter, use getValueAt followed by toString
Object o = getValueAt(row, column);
if (o == null) {
return "";
}
String string = o.toString();
if (string == null) {
return "";
}
return string;
}
@Override
public Integer getIdentifier(int index) {
return index;
}
}
}
protected class JBTableHeader extends JTableHeader {
public JBTableHeader() {
super(JBTable.this.columnModel);
JBTable.this.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(@NotNull PropertyChangeEvent evt) {
JBTableHeader.this.revalidate();
JBTableHeader.this.repaint();
}
});
}
@Override
public void paint(@NotNull Graphics g) {
if (myEnableAntialiasing) {
GraphicsUtil.setupAntialiasing(g);
}
if (!JBTable.this.isEnabled()) {
g = new TableGrayer((Graphics2D)g);
}
super.paint(g);
}
@Override
public String getToolTipText(@NotNull final MouseEvent event) {
final TableModel model = getModel();
if (model instanceof SortableColumnModel) {
final int i = columnAtPoint(event.getPoint());
final int infoIndex = i >= 0 ? convertColumnIndexToModel(i) : -1;
final ColumnInfo[] columnInfos = ((SortableColumnModel)model).getColumnInfos();
final String tooltipText = infoIndex >= 0 && infoIndex < columnInfos.length ? columnInfos[infoIndex].getTooltipText() : null;
if (tooltipText != null) {
return tooltipText;
}
}
return super.getToolTipText(event);
}
}
/**
* Make it possible to disable a JBTable
*
* @author Konstantin Bulenkov
*/
private final class TableGrayer extends Graphics2DDelegate {
public TableGrayer(Graphics2D g2d) {
super(g2d);
}
@Override
public void setColor(Color color) {
if (color != null && (!UIUtil.isUnderDarcula() || !JBTable.this.getBackground().equals(color))) {
//noinspection UseJBColor
color = new Color(UIUtil.getGrayFilter().filterRGB(0, 0, color.getRGB()));
}
super.setColor(color);
}
@NotNull
@Override
public Graphics create() {
return new TableGrayer((Graphics2D)super.create());
}
}
}