blob: 8b148a3d791dd0cf6e7d3b4e69160f73cd8d3eef [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.util.ui.table;
import com.intellij.ide.IdeBundle;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.*;
import com.intellij.ui.table.JBTable;
import com.intellij.ui.table.TableView;
import com.intellij.util.Function;
import com.intellij.util.FunctionUtil;
import com.intellij.util.PlatformIcons;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.ColumnInfo;
import com.intellij.util.ui.ElementProducer;
import com.intellij.util.ui.ListTableModel;
import com.intellij.util.xmlb.SkipDefaultValuesSerializationFilters;
import com.intellij.util.xmlb.XmlSerializer;
import gnu.trove.THashMap;
import gnu.trove.TObjectObjectProcedure;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import java.awt.*;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TableModelEditor<T> implements ElementProducer<T> {
private final TableView<T> table;
private final ToolbarDecorator toolbarDecorator;
private final ItemEditor<T> itemEditor;
private final MyListTableModel<T> model;
public TableModelEditor(@NotNull ColumnInfo[] columns, @NotNull ItemEditor<T> itemEditor, @NotNull String emptyText) {
this(Collections.<T>emptyList(), columns, itemEditor, emptyText);
}
/**
* source will be copied, passed list will not be used directly
*
* Implement {@link DialogItemEditor} instead of {@link ItemEditor} if you want provide dialog to edit.
*/
public TableModelEditor(@NotNull List<T> items, @NotNull ColumnInfo[] columns, @NotNull ItemEditor<T> itemEditor, @NotNull String emptyText) {
this.itemEditor = itemEditor;
model = new MyListTableModel<T>(columns, new ArrayList<T>(items), this);
table = new TableView<T>(model);
table.setDefaultEditor(Enum.class, ComboBoxTableCellEditor.INSTANCE);
table.setStriped(true);
table.setEnableAntialiasing(true);
preferredScrollableViewportHeightInRows(JBTable.PREFERRED_SCROLLABLE_VIEWPORT_HEIGHT_IN_ROWS);
new TableSpeedSearch(table);
if (columns[0].getColumnClass() == Boolean.class && columns[0].getName().isEmpty()) {
TableUtil.setupCheckboxColumn(table.getColumnModel().getColumn(0));
}
boolean needTableHeader = false;
for (ColumnInfo column : columns) {
if (!StringUtil.isEmpty(column.getName())) {
needTableHeader = true;
break;
}
}
if (!needTableHeader) {
table.setTableHeader(null);
}
table.getEmptyText().setText(emptyText);
MyRemoveAction removeAction = new MyRemoveAction();
toolbarDecorator = ToolbarDecorator.createDecorator(table, this).setRemoveAction(removeAction).setRemoveActionUpdater(removeAction);
if (itemEditor instanceof DialogItemEditor) {
addDialogActions();
}
}
public TableModelEditor<T> preferredScrollableViewportHeightInRows(int rows) {
table.setPreferredScrollableViewportSize(new Dimension(200, table.getRowHeight() * rows));
return this;
}
private void addDialogActions() {
toolbarDecorator.setEditAction(new AnActionButtonRunnable() {
@Override
public void run(AnActionButton button) {
T item = table.getSelectedObject();
if (item != null) {
Function<T, T> mutator;
if (model.isMutable(item)) {
mutator = FunctionUtil.id();
}
else {
final int selectedRow = table.getSelectedRow();
mutator = new Function<T, T>() {
@Override
public T fun(T item) {
return model.getMutable(selectedRow, item);
}
};
}
((DialogItemEditor<T>)itemEditor).edit(item, mutator, false);
table.requestFocus();
}
}
}).setEditActionUpdater(new AnActionButtonUpdater() {
@Override
public boolean isEnabled(AnActionEvent e) {
T item = table.getSelectedObject();
return item != null && ((DialogItemEditor<T>)itemEditor).isEditable(item);
}
});
if (((DialogItemEditor)itemEditor).isUseDialogToAdd()) {
toolbarDecorator.setAddAction(new AnActionButtonRunnable() {
@Override
public void run(AnActionButton button) {
T item = createElement();
((DialogItemEditor<T>)itemEditor).edit(item, new Function<T, T>() {
@Override
public T fun(T item) {
model.addRow(item);
return item;
}
}, true);
}
});
}
}
public TableModelEditor<T> disableUpDownActions() {
toolbarDecorator.disableUpDownActions();
return this;
}
public TableModelEditor<T> enabled(boolean value) {
table.setEnabled(value);
return this;
}
public static abstract class DataChangedListener<T> implements TableModelListener {
public abstract void dataChanged(@NotNull ColumnInfo<T, ?> columnInfo, int rowIndex);
@Override
public void tableChanged(TableModelEvent e) {
}
}
public TableModelEditor<T> modelListener(@NotNull DataChangedListener<T> listener) {
model.dataChangedListener = listener;
model.addTableModelListener(listener);
return this;
}
@NotNull
public ListTableModel<T> getModel() {
return model;
}
public static abstract class ItemEditor<T> {
/**
* Used for "copy" and "in place edit" actions.
*
* You must perform deep clone in case of "add" operation, but in case of "in place edit" you should copy only exposed (via column) properties.
*/
public abstract T clone(@NotNull T item, boolean forInPlaceEditing);
@NotNull
/**
* Class must have empty constructor.
*/
public abstract Class<T> getItemClass();
public boolean isRemovable(@NotNull T item) {
return true;
}
}
public static abstract class DialogItemEditor<T> extends ItemEditor<T> {
public abstract void edit(@NotNull T item, @NotNull Function<T, T> mutator, boolean isAdd);
public abstract void applyEdited(@NotNull T oldItem, @NotNull T newItem);
public boolean isEditable(@NotNull T item) {
return true;
}
public boolean isUseDialogToAdd() {
return false;
}
}
@NotNull
public static <T> T cloneUsingXmlSerialization(@NotNull T oldItem, @NotNull T newItem) {
Element serialized = XmlSerializer.serialize(oldItem, new SkipDefaultValuesSerializationFilters());
if (!JDOMUtil.isEmpty(serialized)) {
XmlSerializer.deserializeInto(newItem, serialized);
}
return newItem;
}
private static final class MyListTableModel<T> extends ListTableModel<T> {
private List<T> items;
private final TableModelEditor<T> editor;
private final THashMap<T, T> modifiedToOriginal = new THashMap<T, T>();
private DataChangedListener<T> dataChangedListener;
public MyListTableModel(@NotNull ColumnInfo[] columns, @NotNull List<T> items, @NotNull TableModelEditor<T> editor) {
super(columns, items);
this.items = items;
this.editor = editor;
}
@Override
public void setItems(@NotNull List<T> items) {
modifiedToOriginal.clear();
this.items = items;
super.setItems(items);
}
@Override
public void removeRow(int index) {
modifiedToOriginal.remove(getItem(index));
super.removeRow(index);
}
@Override
public void setValueAt(Object newValue, int rowIndex, int columnIndex) {
if (rowIndex < getRowCount()) {
@SuppressWarnings("unchecked")
ColumnInfo<T, Object> column = (ColumnInfo<T, Object>)getColumnInfos()[columnIndex];
T item = getItem(rowIndex);
Object oldValue = column.valueOf(item);
if (column.getColumnClass() == String.class
? !Comparing.strEqual(((String)oldValue), ((String)newValue))
: !Comparing.equal(oldValue, newValue)) {
column.setValue(getMutable(rowIndex, item), newValue);
if (dataChangedListener != null) {
dataChangedListener.dataChanged(column, rowIndex);
}
}
}
}
private T getMutable(int rowIndex, T item) {
if (isMutable(item)) {
return item;
}
else {
T mutable = editor.itemEditor.clone(item, true);
modifiedToOriginal.put(mutable, item);
items.set(rowIndex, mutable);
return mutable;
}
}
private boolean isMutable(T item) {
return modifiedToOriginal.containsKey(item);
}
public boolean isModified(@NotNull List<T> oldItems) {
if (items.size() != oldItems.size()) {
return true;
}
else {
for (int i = 0, size = items.size(); i < size; i++) {
if (!items.get(i).equals(oldItems.get(i))) {
return true;
}
}
}
return false;
}
@NotNull
public List<T> apply() {
if (modifiedToOriginal.isEmpty()) {
return items;
}
@SuppressWarnings("unchecked")
final ColumnInfo<T, Object>[] columns = getColumnInfos();
modifiedToOriginal.forEachEntry(new TObjectObjectProcedure<T, T>() {
@Override
public boolean execute(T newItem, @Nullable T oldItem) {
if (oldItem == null) {
// it is added item, we don't need to sync
return true;
}
for (ColumnInfo<T, Object> column : columns) {
if (column.isCellEditable(newItem)) {
column.setValue(oldItem, column.valueOf(newItem));
}
}
if (editor.itemEditor instanceof DialogItemEditor) {
((DialogItemEditor<T>)editor.itemEditor).applyEdited(oldItem, newItem);
}
items.set(ContainerUtil.indexOfIdentity(items, newItem), oldItem);
return true;
}
});
modifiedToOriginal.clear();
return items;
}
}
public abstract static class EditableColumnInfo<Item, Aspect> extends ColumnInfo<Item, Aspect> {
public EditableColumnInfo(@NotNull String name) {
super(name);
}
public EditableColumnInfo() {
super("");
}
@Override
public boolean isCellEditable(Item item) {
return true;
}
}
@NotNull
public JComponent createComponent() {
return toolbarDecorator.addExtraAction(
new ToolbarDecorator.ElementActionButton(IdeBundle.message("button.copy"), PlatformIcons.COPY_ICON) {
@Override
public void actionPerformed(AnActionEvent e) {
TableUtil.stopEditing(table);
List<T> selectedItems = table.getSelectedObjects();
if (selectedItems.isEmpty()) {
return;
}
for (T item : selectedItems) {
model.addRow(itemEditor.clone(item, false));
}
table.requestFocus();
TableUtil.updateScroller(table, false);
}
}
).createPanel();
}
@Override
public T createElement() {
try {
Constructor<T> constructor = itemEditor.getItemClass().getDeclaredConstructor();
try {
constructor.setAccessible(true);
}
catch (SecurityException ignored) {
return itemEditor.getItemClass().newInstance();
}
return constructor.newInstance();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public boolean canCreateElement() {
return true;
}
public boolean isModified(@NotNull List<T> oldItems) {
return model.isModified(oldItems);
}
public void selectItem(@NotNull final T item) {
table.clearSelection();
final Ref<T> ref;
if (model.modifiedToOriginal.isEmpty()) {
ref = null;
}
else {
ref = Ref.create();
model.modifiedToOriginal.forEachEntry(new TObjectObjectProcedure<T, T>() {
@Override
public boolean execute(T modified, T original) {
if (item == original) {
ref.set(modified);
}
return ref.isNull();
}
});
}
table.addSelection(ref == null || ref.isNull() ? item : ref.get());
}
@NotNull
public List<T> apply() {
return model.apply();
}
public void reset(@NotNull List<T> items) {
model.setItems(new ArrayList<T>(items));
}
private class MyRemoveAction implements AnActionButtonRunnable, AnActionButtonUpdater, TableUtil.ItemChecker {
@Override
public void run(AnActionButton button) {
if (TableUtil.doRemoveSelectedItems(table, model, this)) {
table.requestFocus();
TableUtil.updateScroller(table, false);
}
}
@Override
public boolean isOperationApplyable(@NotNull TableModel ignored, int row) {
T item = model.getItem(row);
return item != null && itemEditor.isRemovable(item);
}
@Override
public boolean isEnabled(AnActionEvent e) {
for (T item : table.getSelectedObjects()) {
if (itemEditor.isRemovable(item)) {
return true;
}
}
return false;
}
}
}