blob: bccbd7b144acd721e84d33a6961e64c02ec2d715 [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.ide.util;
import com.intellij.ui.*;
import com.intellij.ui.table.JBTable;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.ComponentWithEmptyText;
import com.intellij.util.ui.StatusText;
import com.intellij.util.ui.Table;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.*;
import java.util.List;
public class MultiStateElementsChooser<T, S> extends JPanel implements ComponentWithEmptyText, ComponentWithExpandableItems<TableCell> {
private MarkStateDescriptor<T, S> myMarkStateDescriptor;
private JBTable myTable = null;
private MyTableModel myTableModel = null;
private boolean myColorUnmarkedElements = true;
private final List<ElementsMarkStateListener<T, S>> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final Map<T,ElementProperties> myElementToPropertiesMap = new HashMap<T, ElementProperties>();
private final Map<T, Boolean> myDisabledMap = new HashMap<T, Boolean>();
public interface ElementsMarkStateListener<T, S> {
void elementMarkChanged(T element, S markState);
}
public interface MarkStateDescriptor<T, S> {
@NotNull
S getDefaultState(@NotNull T element);
@NotNull
S getNextState(@NotNull T element, @NotNull S state);
@Nullable
S getNextState(@NotNull Map<T, S> elementsWithStates);
boolean isMarked(@NotNull S state);
@Nullable
S getMarkState(@Nullable Object value);
@Nullable
TableCellRenderer getMarkRenderer();
}
public MultiStateElementsChooser(final boolean elementsCanBeMarked, MarkStateDescriptor<T, S> markStateDescriptor) {
this(null, null, elementsCanBeMarked, markStateDescriptor);
}
public MultiStateElementsChooser(List<T> elements, S markState, MarkStateDescriptor<T, S> markStateDescriptor) {
this(elements, markState, true, markStateDescriptor);
}
private MultiStateElementsChooser(@Nullable List<T> elements,
S markState,
boolean elementsCanBeMarked,
MarkStateDescriptor<T, S> markStateDescriptor) {
super(new BorderLayout());
myMarkStateDescriptor = markStateDescriptor;
myTableModel = new MyTableModel(elementsCanBeMarked);
myTable = new Table(myTableModel);
myTable.setShowGrid(false);
myTable.setIntercellSpacing(new Dimension(0, 0));
myTable.setTableHeader(null);
myTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
myTable.setColumnSelectionAllowed(false);
JScrollPane pane = ScrollPaneFactory.createScrollPane(myTable);
pane.setPreferredSize(new Dimension(100, 155));
TableColumnModel columnModel = myTable.getColumnModel();
if (elementsCanBeMarked) {
TableColumn checkMarkColumn = columnModel.getColumn(myTableModel.CHECK_MARK_COLUM_INDEX);
TableUtil.setupCheckboxColumn(checkMarkColumn);
TableCellRenderer checkMarkRenderer = myMarkStateDescriptor.getMarkRenderer();
if (checkMarkRenderer == null) {
checkMarkRenderer = new CheckMarkColumnCellRenderer(myTable.getDefaultRenderer(Boolean.class));
}
checkMarkColumn.setCellRenderer(checkMarkRenderer);
}
columnModel.getColumn(myTableModel.ELEMENT_COLUMN_INDEX).setCellRenderer(new MyElementColumnCellRenderer());
add(pane, BorderLayout.CENTER);
myTable.registerKeyboardAction(
new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
final int[] selectedRows = myTable.getSelectedRows();
Map<T, S> selectedElements = new LinkedHashMap<T, S>(selectedRows.length);
for (int selectedRow : selectedRows) {
selectedElements.put(myTableModel.getElementAt(selectedRow), myTableModel.getElementMarkState(selectedRow));
}
S nextState = myMarkStateDescriptor.getNextState(selectedElements);
if (nextState != null) {
myTableModel.setMarkState(selectedRows, nextState);
}
}
},
KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0),
JComponent.WHEN_FOCUSED
);
final SpeedSearchBase<JBTable> speedSearch = new SpeedSearchBase<JBTable>(myTable) {
@Override
public int getSelectedIndex() {
return myTable.getSelectedRow();
}
@Override
protected int convertIndexToModel(int viewIndex) {
return myTable.convertRowIndexToModel(viewIndex);
}
@Override
public Object[] getAllElements() {
final int count = myTableModel.getRowCount();
Object[] elements = new Object[count];
for (int idx = 0; idx < count; idx++) {
elements[idx] = myTableModel.getElementAt(idx);
}
return elements;
}
@Override
public String getElementText(Object element) {
return getItemText((T)element);
}
@Override
public void selectElement(Object element, String selectedText) {
final int count = myTableModel.getRowCount();
for (int row = 0; row < count; row++) {
if (element.equals(myTableModel.getElementAt(row))) {
final int viewRow = myTable.convertRowIndexToView(row);
myTable.getSelectionModel().setSelectionInterval(viewRow, viewRow);
TableUtil.scrollSelectionToVisible(myTable);
break;
}
}
}
};
speedSearch.setComparator(new SpeedSearchComparator(false));
setElements(elements, markState);
installActions(myTable);
}
private static void installActions(JTable table) {
InputMap inputMap = table.getInputMap(WHEN_FOCUSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), "selectLastRow");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), "selectFirstRow");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.SHIFT_DOWN_MASK), "selectFirstRowExtendSelection");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.SHIFT_DOWN_MASK), "selectLastRowExtendSelection");
}
@NotNull
@Override
public StatusText getEmptyText() {
return myTable.getEmptyText();
}
@NotNull
@Override
public ExpandableItemsHandler<TableCell> getExpandableItemsHandler() {
return myTable.getExpandableItemsHandler();
}
@Override
public void setExpandableItemsEnabled(boolean enabled) {
myTable.setExpandableItemsEnabled(enabled);
}
public void setSingleSelectionMode() {
myTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
public void refresh() {
myTableModel.fireTableDataChanged();
}
public void refresh(T element) {
final int row = myTableModel.getElementRow(element);
if (row >= 0) {
myTableModel.fireTableRowsUpdated(row, row);
}
}
private int[] mySavedSelection = null;
public void saveSelection() {
mySavedSelection = myTable.getSelectedRows();
}
public void restoreSelection() {
if (mySavedSelection != null) {
TableUtil.selectRows(myTable, mySavedSelection);
mySavedSelection = null;
}
}
public boolean isColorUnmarkedElements() {
return myColorUnmarkedElements;
}
public void setColorUnmarkedElements(boolean colorUnmarkedElements) {
myColorUnmarkedElements = colorUnmarkedElements;
}
public void addElementsMarkListener(ElementsMarkStateListener<T, S> listener) {
myListeners.add(listener);
}
public void removeElementsMarkListener(ElementsMarkStateListener<T, S> listener) {
myListeners.remove(listener);
}
public void addListSelectionListener(ListSelectionListener listener) {
myTable.getSelectionModel().addListSelectionListener(listener);
}
public void removeListSelectionListener(ListSelectionListener listener) {
myTable.getSelectionModel().removeListSelectionListener(listener);
}
public void addElement(T element, final S markState) {
addElement(element, markState, element instanceof ElementProperties ? (ElementProperties)element : null);
}
/**
* Gets element mark state
* @param element an element to test
* @return state of element
*/
public S getElementMarkState(T element) {
final int elementRow = myTableModel.getElementRow(element);
return myTableModel.getElementMarkState(elementRow);
}
/**
* Update element mark state
* @param element an element to test
* @param markState a new value of mark state
*/
public void setElementMarkState(T element, S markState) {
final int elementRow = myTableModel.getElementRow(element);
myTableModel.setMarkState(elementRow, markState);
}
public void removeElement(T element) {
final int elementRow = myTableModel.getElementRow(element);
if (elementRow < 0) {
return; // no such element
}
final boolean wasSelected = myTable.getSelectionModel().isSelectedIndex(elementRow);
myTableModel.removeElement(element);
myElementToPropertiesMap.remove(element);
if (wasSelected) {
final int rowCount = myTableModel.getRowCount();
if (rowCount > 0) {
selectRow(elementRow % rowCount);
}
else {
myTable.getSelectionModel().clearSelection();
}
}
myTable.requestFocus();
}
public void removeAllElements() {
myTableModel.removeAllElements();
myTable.getSelectionModel().clearSelection();
}
private void selectRow(final int row) {
myTable.getSelectionModel().setSelectionInterval(row, row);
myTable.scrollRectToVisible(myTable.getCellRect(row, 0, true));
}
public void moveElement(T element, int newRow) {
final int elementRow = myTableModel.getElementRow(element);
if (elementRow < 0 || elementRow == newRow || newRow < 0 || newRow >= myTableModel.getRowCount()) {
return;
}
final boolean wasSelected = myTable.getSelectionModel().isSelectedIndex(elementRow);
myTableModel.changeElementRow(element, newRow);
if (wasSelected) {
selectRow(newRow);
}
}
public interface ElementProperties {
@Nullable
Icon getIcon();
@Nullable
Color getColor();
}
public void addElement(T element, final S markState, ElementProperties elementProperties) {
myTableModel.addElement(element, markState);
myElementToPropertiesMap.put(element, elementProperties);
selectRow(myTableModel.getRowCount() - 1);
myTable.requestFocus();
}
public void setElementProperties(T element, ElementProperties properties) {
myElementToPropertiesMap.put(element, properties);
}
public void setElements(List<T> elements, S markState) {
myTableModel.clear();
myTableModel.addElements(elements, markState);
}
@Nullable
public T getSelectedElement() {
final int selectedRow = getSelectedElementRow();
return selectedRow < 0? null : myTableModel.getElementAt(selectedRow);
}
public int getSelectedElementRow() {
return myTable.getSelectedRow();
}
@NotNull
public List<T> getSelectedElements() {
final List<T> elements = new ArrayList<T>();
final int[] selectedRows = myTable.getSelectedRows();
for (int selectedRow : selectedRows) {
if (selectedRow < 0) {
continue;
}
elements.add(myTableModel.getElementAt(selectedRow));
}
return elements;
}
public void selectElements(Collection<? extends T> elements) {
if (elements.isEmpty()) {
myTable.clearSelection();
return;
}
final int[] rows = getElementsRows(elements);
TableUtil.selectRows(myTable, rows);
TableUtil.scrollSelectionToVisible(myTable);
myTable.requestFocus();
}
private int[] getElementsRows(final Collection<? extends T> elements) {
final int[] rows = new int[elements.size()];
int index = 0;
for (final T element : elements) {
rows[index++] = myTable.convertRowIndexToView(myTableModel.getElementRow(element));
}
return rows;
}
public void markElements(Collection<T> elements, S markState) {
myTableModel.setMarkState(getElementsRows(elements), markState);
}
@NotNull
public Map<T, S> getElementMarkStates() {
final int count = myTableModel.getRowCount();
Map<T, S> elements = new LinkedHashMap<T, S>();
for (int idx = 0; idx < count; idx++) {
final T element = myTableModel.getElementAt(idx);
elements.put(element, myTableModel.getElementMarkState(idx));
}
return elements;
}
public void sort(Comparator<T> comparator) {
myTableModel.sort(comparator);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
myTable.setRowSelectionAllowed(enabled);
myTableModel.fireTableDataChanged();
}
public void stopEditing() {
TableCellEditor editor = myTable.getCellEditor();
if (editor != null) {
editor.stopCellEditing();
}
}
public JComponent getComponent() {
return myTable;
}
public void setAllElementsMarked(S markState) {
final int[] rows = new int[myTableModel.getRowCount()];
for (int idx = 0; idx < rows.length; idx++) {
rows[idx] = idx;
}
myTableModel.setMarkState(rows, markState);
}
private void notifyElementMarked(T element, S markState) {
for (ElementsMarkStateListener<T, S> listener : myListeners) {
listener.elementMarkChanged(element, markState);
}
}
public void clear() {
myTableModel.clear();
myElementToPropertiesMap.clear();
}
public int getElementCount() {
return myTableModel.getRowCount();
}
public T getElementAt(int row) {
return myTableModel.getElementAt(row);
}
public void disableElement(T element) {
myDisabledMap.put(element, Boolean.TRUE);
}
private final class MyTableModel extends AbstractTableModel {
private final List<T> myElements = new ArrayList<T>();
private final Map<T, S> myMarkedMap = new HashMap<T, S>();
public final int CHECK_MARK_COLUM_INDEX;
public final int ELEMENT_COLUMN_INDEX;
private final boolean myElementsCanBeMarked;
public MyTableModel(final boolean elementsCanBeMarked) {
myElementsCanBeMarked = elementsCanBeMarked;
if (elementsCanBeMarked) {
CHECK_MARK_COLUM_INDEX = 0;
ELEMENT_COLUMN_INDEX = 1;
}
else {
CHECK_MARK_COLUM_INDEX = -1;
ELEMENT_COLUMN_INDEX = 0;
}
}
public void sort(Comparator<T> comparator) {
Collections.sort(myElements, comparator);
fireTableDataChanged();
}
public T getElementAt(int index) {
return myElements.get(index);
}
public S getElementMarkState(int index) {
final T element = myElements.get(index);
return myMarkedMap.get(element);
}
private void addElement(T element, S markState) {
myElements.add(element);
myMarkedMap.put(element, notNullMarkState(element, markState));
int row = myElements.size() - 1;
fireTableRowsInserted(row, row);
}
private void addElements(@Nullable List<T> elements, S markState) {
if (elements == null || elements.isEmpty()) {
return;
}
for (final T element : elements) {
myElements.add(element);
myMarkedMap.put(element, notNullMarkState(element, markState));
}
fireTableRowsInserted(myElements.size() - elements.size(), myElements.size() - 1);
}
public void removeElement(T element) {
final boolean reallyRemoved = myElements.remove(element);
if (reallyRemoved) {
myMarkedMap.remove(element);
fireTableDataChanged();
}
}
public void changeElementRow(T element, int row) {
final boolean reallyRemoved = myElements.remove(element);
if (reallyRemoved) {
myElements.add(row, element);
fireTableDataChanged();
}
}
public int getElementRow(T element) {
return myElements.indexOf(element);
}
public void removeAllElements() {
myElements.clear();
fireTableDataChanged();
}
public void removeRows(int[] rows) {
final List<T> toRemove = new ArrayList<T>();
for (int row : rows) {
final T element = myElements.get(row);
toRemove.add(element);
myMarkedMap.remove(element);
}
myElements.removeAll(toRemove);
fireTableDataChanged();
}
@Override
public int getRowCount() {
return myElements.size();
}
@Override
public int getColumnCount() {
return myElementsCanBeMarked? 2 : 1;
}
@Override
@Nullable
public Object getValueAt(int rowIndex, int columnIndex) {
T element = myElements.get(rowIndex);
if (columnIndex == ELEMENT_COLUMN_INDEX) {
return element;
}
if (columnIndex == CHECK_MARK_COLUM_INDEX) {
return myMarkedMap.get(element);
}
return null;
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
if (columnIndex == CHECK_MARK_COLUM_INDEX) {
S nextState = myMarkStateDescriptor.getMarkState(aValue);
if (nextState == null) {
T element = myTableModel.getElementAt(rowIndex);
S currentState = myTableModel.getElementMarkState(rowIndex);
nextState = myMarkStateDescriptor.getNextState(element, currentState);
}
setMarkState(rowIndex, nextState);
}
}
private void setMarkState(int rowIndex, final S markState) {
final T element = myElements.get(rowIndex);
final S newValue = notNullMarkState(element, markState);
final S prevValue = myMarkedMap.put(element, newValue);
fireTableRowsUpdated(rowIndex, rowIndex);
if (!newValue.equals(prevValue)) {
notifyElementMarked(element, newValue);
}
}
private void setMarkState(int[] rows, final S markState) {
if (rows == null || rows.length == 0) {
return;
}
int firstRow = Integer.MAX_VALUE;
int lastRow = Integer.MIN_VALUE;
for (final int row : rows) {
final T element = myElements.get(row);
final S newValue = notNullMarkState(element, markState);
final S prevValue = myMarkedMap.put(element, newValue);
if (!newValue.equals(prevValue)) {
notifyElementMarked(element, newValue);
}
firstRow = Math.min(firstRow, row);
lastRow = Math.max(lastRow, row);
}
fireTableRowsUpdated(firstRow, lastRow);
}
@NotNull
private S notNullMarkState(T element, S markState) {
return markState != null ? markState : myMarkStateDescriptor.getDefaultState(element);
}
@Override
public Class getColumnClass(int columnIndex) {
if (columnIndex == CHECK_MARK_COLUM_INDEX) {
return Boolean.class;
}
return super.getColumnClass(columnIndex);
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
if (!isEnabled() || columnIndex != CHECK_MARK_COLUM_INDEX) {
return false;
}
final T o = (T)getValueAt(rowIndex, ELEMENT_COLUMN_INDEX);
return myDisabledMap.get(o) == null;
}
public void clear() {
myElements.clear();
myMarkedMap.clear();
fireTableDataChanged();
}
}
protected String getItemText(@NotNull T value) {
return value.toString();
}
@Nullable
protected Icon getItemIcon(@NotNull T value) {
return null;
}
private class MyElementColumnCellRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
final Color color = UIUtil.getTableFocusCellBackground();
Component component;
T t = (T)value;
try {
UIManager.put(UIUtil.TABLE_FOCUS_CELL_BACKGROUND_PROPERTY, table.getSelectionBackground());
component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setText(t != null ? getItemText(t) : "");
if (component instanceof JLabel) {
((JLabel)component).setBorder(noFocusBorder);
}
}
finally {
UIManager.put(UIUtil.TABLE_FOCUS_CELL_BACKGROUND_PROPERTY, color);
}
final MyTableModel model = (MyTableModel)table.getModel();
component.setEnabled(MultiStateElementsChooser.this.isEnabled() &&
(!myColorUnmarkedElements || myMarkStateDescriptor.isMarked(model.getElementMarkState(row))));
final ElementProperties properties = myElementToPropertiesMap.get(t);
if (component instanceof JLabel) {
final Icon icon = properties != null ? properties.getIcon() : t != null ? getItemIcon(t) : null;
JLabel label = (JLabel)component;
label.setIcon(icon);
label.setDisabledIcon(icon);
}
component.setForeground(properties != null && properties.getColor() != null ?
properties.getColor() :
isSelected ? table.getSelectionForeground() : table.getForeground());
return component;
}
}
private class CheckMarkColumnCellRenderer implements TableCellRenderer {
private final TableCellRenderer myDelegate;
public CheckMarkColumnCellRenderer(TableCellRenderer delegate) {
myDelegate = delegate;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component component = myDelegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
component.setEnabled(isEnabled());
if (component instanceof JComponent) {
((JComponent)component).setBorder(null);
}
return component;
}
}
}