blob: 41a72cc9d4277b05f5866e34c44d0b8bc2ed13a4 [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.designer.propertyTable;
import com.intellij.designer.model.ErrorInfo;
import com.intellij.designer.model.PropertiesContainer;
import com.intellij.designer.model.Property;
import com.intellij.designer.model.PropertyContext;
import com.intellij.designer.propertyTable.renderers.LabelPropertyRenderer;
import com.intellij.ide.ui.search.SearchUtil;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
import com.intellij.ui.*;
import com.intellij.ui.table.JBTable;
import com.intellij.util.PairFunction;
import com.intellij.util.ThrowableRunnable;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.plaf.TableUI;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.MessageFormat;
import java.util.*;
import java.util.List;
/**
* @author Alexander Lobas
*/
public abstract class PropertyTable extends JBTable {
private static final Logger LOG = Logger.getInstance("#com.intellij.designer.propertyTable.PropertyTable");
private static final Comparator<String> GROUP_COMPARATOR = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return StringUtil.compare(o1, o2, true);
}
};
private static final Comparator<Property> PROPERTY_COMPARATOR = new Comparator<Property>() {
@Override
public int compare(Property o1, Property o2) {
return StringUtil.compare(o1.getName(), o2.getName(), true);
}
};
private boolean mySorted;
private boolean myShowGroups;
private boolean myShowExpertProperties;
private String[] myColumnNames = new String[]{"Property", "Value"};
private final TableSpeedSearch mySpeedSearch;
private final AbstractTableModel myModel = new PropertyTableModel();
protected List<PropertiesContainer> myContainers = Collections.emptyList();
protected List<Property> myProperties = Collections.emptyList();
protected final Set<String> myExpandedProperties = new HashSet<String>();
private boolean mySkipUpdate;
private boolean myStoppingEditing;
private final TableCellRenderer myCellRenderer = new PropertyCellRenderer();
private final PropertyCellEditor myCellEditor = new PropertyCellEditor();
private final PropertyEditorListener myPropertyEditorListener = new PropertyCellEditorListener();
public PropertyTable() {
setModel(myModel);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setShowColumns(false);
setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
setShowVerticalLines(false);
setIntercellSpacing(new Dimension(0, 1));
setGridColor(UIUtil.getSlightlyDarkerColor(getBackground()));
setColumnSelectionAllowed(false);
setCellSelectionEnabled(false);
setRowSelectionAllowed(true);
addMouseListener(new MouseTableListener());
mySpeedSearch = new TableSpeedSearch(this, new PairFunction<Object, Cell, String>() {
@Override
public String fun(Object object, Cell cell) {
if (cell.column != 0) return null;
if (object instanceof GroupProperty) return null;
return ((Property)object).getName();
}
}) {
@Override
protected void selectElement(Object element, String selectedText) {
super.selectElement(element, selectedText);
repaint(PropertyTable.this.getVisibleRect());
}
};
mySpeedSearch.setComparator(new SpeedSearchComparator(false, false));
// TODO: Updates UI after LAF updated
}
public void setColumnNames(String... columnNames) {
if (columnNames.length != 2) throw new IllegalArgumentException("Invalid number of columns. Expected 2, got " + columnNames.length);
myColumnNames = columnNames;
TableColumnModel mmodel = getColumnModel();
for (int i = 0; i < columnNames.length; i++) {
mmodel.getColumn(i).setHeaderValue(columnNames[i]);
}
}
public void setSorted(boolean sorted) {
mySorted = sorted;
update();
}
public boolean isSorted() {
return mySorted;
}
public void setShowGroups(boolean showGroups) {
myShowGroups = showGroups;
update();
}
public boolean isShowGroups() {
return myShowGroups;
}
public void showExpert(boolean showExpert) {
myShowExpertProperties = showExpert;
update();
}
public boolean isShowExpertProperties() {
return myShowExpertProperties;
}
public void setUI(TableUI ui) {
super.setUI(ui);
// Customize action and input maps
ActionMap actionMap = getActionMap();
setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS));
setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS));
InputMap focusedInputMap = getInputMap(JComponent.WHEN_FOCUSED);
InputMap ancestorInputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
actionMap.put("selectPreviousRow", new MySelectNextPreviousRowAction(false));
actionMap.put("selectNextRow", new MySelectNextPreviousRowAction(true));
actionMap.put("startEditing", new MyStartEditingAction());
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), "startEditing");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0));
actionMap.put("smartEnter", new MyEnterAction());
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "smartEnter");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
ancestorInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
actionMap.put("restoreDefault", new MyRestoreDefaultAction());
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "restoreDefault");
ancestorInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "restoreDefault");
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "restoreDefault");
ancestorInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "restoreDefault");
actionMap.put("expandCurrent", new MyExpandCurrentAction(true, false));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), "expandCurrent");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0));
actionMap.put("expandCurrentRight", new MyExpandCurrentAction(true, true));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "expandCurrentRight");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0), "expandCurrentRight");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0));
actionMap.put("collapseCurrent", new MyExpandCurrentAction(false, false));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), "collapseCurrent");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0));
actionMap.put("collapseCurrentLeft", new MyExpandCurrentAction(false, true));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "collapseCurrentLeft");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0), "collapseCurrentLeft");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0));
}
public TableCellRenderer getCellRenderer(int row, int column) {
return myCellRenderer;
}
public void restoreDefaultValue() {
final Property property = getSelectionProperty();
if (property != null) {
if (isEditing()) {
cellEditor.stopCellEditing();
}
doRestoreDefault(new ThrowableRunnable<Exception>() {
@Override
public void run() throws Exception {
for (PropertiesContainer component : myContainers) {
if (!property.isDefaultRecursively(component)) {
property.setDefaultValue(component);
}
}
}
});
repaint();
}
}
protected abstract boolean doRestoreDefault(ThrowableRunnable<Exception> runnable);
@Nullable
public ErrorInfo getErrorInfoForRow(int row) {
if (myContainers.size() != 1) {
return null;
}
Property property = myProperties.get(row);
if (property.getParent() != null) {
return null;
}
for (ErrorInfo errorInfo : getErrors(myContainers.get(0))) {
if (property.getName().equals(errorInfo.getPropertyName())) {
return errorInfo;
}
}
return null;
}
protected abstract List<ErrorInfo> getErrors(@NotNull PropertiesContainer container);
@Override
public String getToolTipText(MouseEvent event) {
int row = rowAtPoint(event.getPoint());
if (row != -1 && !myProperties.isEmpty()) {
ErrorInfo errorInfo = getErrorInfoForRow(row);
if (errorInfo != null) {
return errorInfo.getName();
}
if (columnAtPoint(event.getPoint()) == 0) {
String tooltip = myProperties.get(row).getTooltip();
if (tooltip != null) {
return tooltip;
}
}
}
return super.getToolTipText(event);
}
//////////////////////////////////////////////////////////////////////////////////////////
//
//
//
//////////////////////////////////////////////////////////////////////////////////////////
@Nullable
protected PropertyContext getPropertyContext() {
return null;
}
public void update() {
update(myContainers, null);
}
public void update(@NotNull List<? extends PropertiesContainer> containers, @Nullable Property initialSelection) {
update(containers, initialSelection, true);
}
private void update(@NotNull List<? extends PropertiesContainer> containers, @Nullable Property initialSelection, boolean finishEditing) {
if (finishEditing) {
finishEditing();
}
if (mySkipUpdate) {
return;
}
mySkipUpdate = true;
try {
if (finishEditing && isEditing()) {
cellEditor.stopCellEditing();
}
Property selection = initialSelection != null ? initialSelection : getSelectionProperty();
myContainers = new ArrayList<PropertiesContainer>(containers);
fillProperties();
myModel.fireTableDataChanged();
restoreSelection(selection);
}
finally {
mySkipUpdate = false;
}
}
private void sortPropertiesAndCreateGroups(List<Property> rootProperties) {
if (!mySorted && !myShowGroups) return;
Collections.sort(rootProperties, new Comparator<Property>() {
@Override
public int compare(Property o1, Property o2) {
if (o1.getParent() != null || o2.getParent() != null) {
if (o1.getParent() == o2) return -1;
if (o2.getParent() == o1) return 1;
return 0;
}
if (myShowGroups) {
int result = getGroupComparator().compare(o1.getGroup(), o2.getGroup());
if (result != 0) return result;
}
return mySorted ? getPropertyComparator().compare(o1, o2) : 0;
}
});
if (myShowGroups) {
for (int i = 0; i < rootProperties.size() - 1; i++) {
Property prev = i == 0 ? null : rootProperties.get(i - 1);
Property each = rootProperties.get(i);
String eachGroup = each.getGroup();
String prevGroup = prev == null ? null : prev.getGroup();
if (prevGroup != null || eachGroup != null) {
if (!StringUtil.equalsIgnoreCase(eachGroup, prevGroup)) {
rootProperties.add(i, new GroupProperty(each.getGroup()));
i++;
}
}
}
}
}
@NotNull
protected Comparator<String> getGroupComparator() {
return GROUP_COMPARATOR;
}
@NotNull
protected Comparator<Property> getPropertyComparator() {
return PROPERTY_COMPARATOR;
}
protected List<Property> getProperties(PropertiesContainer component) {
return component.getProperties();
}
private void restoreSelection(Property selection) {
List<Property> propertyPath = new ArrayList<Property>(2);
while (selection != null) {
propertyPath.add(0, selection);
selection = selection.getParent();
}
int indexToSelect = -1;
int size = propertyPath.size();
for (int i = 0; i < size; i++) {
int index = findFullPathProperty(myProperties, propertyPath.get(i));
if (index == -1) {
break;
}
if (i == size - 1) {
indexToSelect = index;
}
else {
expand(index);
}
}
if (indexToSelect != -1) {
getSelectionModel().setSelectionInterval(indexToSelect, indexToSelect);
}
else if (getRowCount() > 0) {
indexToSelect = 0;
for (int i = 0; i < myProperties.size(); i++) {
if (!(myProperties.get(i) instanceof GroupProperty)) {
indexToSelect = i;
break;
}
}
getSelectionModel().setSelectionInterval(indexToSelect, indexToSelect);
}
TableUtil.scrollSelectionToVisible(this);
}
private void fillProperties() {
myProperties = new ArrayList<Property>();
int size = myContainers.size();
if (size > 0) {
List<Property> rootProperties = new ArrayList<Property>();
for (Property each : (Iterable<? extends Property>)getProperties(myContainers.get(0))) {
addIfNeeded(getCurrentComponent(), each, rootProperties);
}
sortPropertiesAndCreateGroups(rootProperties);
for (Property property : rootProperties) {
myProperties.add(property);
addExpandedChildren(getCurrentComponent(), property, myProperties);
}
if (size > 1) {
for (Iterator<Property> I = myProperties.iterator(); I.hasNext(); ) {
if (!I.next().availableFor(myContainers)) {
I.remove();
}
}
for (int i = 1; i < size; i++) {
List<Property> otherProperties = new ArrayList<Property>();
fillProperties(myContainers.get(i), otherProperties);
for (Iterator<Property> I = myProperties.iterator(); I.hasNext(); ) {
Property addedProperty = I.next();
int index = findFullPathProperty(otherProperties, addedProperty);
if (index == -1) {
I.remove();
continue;
}
Property testProperty = otherProperties.get(index);
if (!addedProperty.getClass().equals(testProperty.getClass())) {
I.remove();
continue;
}
List<Property> addedChildren = getChildren(addedProperty);
List<Property> testChildren = getChildren(testProperty);
int addedChildrenSize = addedChildren.size();
if (addedChildrenSize != testChildren.size()) {
I.remove();
continue;
}
for (int j = 0; j < addedChildrenSize; j++) {
if (!addedChildren.get(j).getName().equals(testChildren.get(j).getName())) {
I.remove();
break;
}
}
}
}
}
}
}
private void fillProperties(PropertiesContainer<?> component, List<Property> properties) {
for (Property each : getProperties(component)) {
if (addIfNeeded(component, each, properties)) {
addExpandedChildren(component, each, properties);
}
}
}
private void addExpandedChildren(PropertiesContainer<?> component, Property property, List<Property> properties) {
if (isExpanded(property)) {
for (Property child : getChildren(property)) {
if (addIfNeeded(component, child, properties)) {
addExpandedChildren(component, child, properties);
}
}
}
}
private boolean addIfNeeded(PropertiesContainer<?> component, Property property, List<Property> properties) {
if (property.isExpert() && !myShowExpertProperties) {
try {
if (property.isDefaultRecursively(component)) {
return false;
}
}
catch (Throwable ignore) {
}
}
properties.add(property);
return true;
}
@Nullable
public static Property findProperty(List<Property> properties, String name) {
for (Property property : properties) {
if (name.equals(property.getName())) {
return property;
}
}
return null;
}
public static int findProperty(List<Property> properties, Property property) {
String name = property.getName();
int size = properties.size();
for (int i = 0; i < size; i++) {
Property nextProperty = properties.get(i);
if (Comparing.equal(nextProperty.getGroup(), property.getGroup()) && name.equals(nextProperty.getName())) {
return i;
}
}
return -1;
}
private static int findFullPathProperty(List<Property> properties, Property property) {
Property parent = property.getParent();
if (parent == null) {
return findProperty(properties, property);
}
String name = getFullPathName(property);
int size = properties.size();
for (int i = 0; i < size; i++) {
if (name.equals(getFullPathName(properties.get(i)))) {
return i;
}
}
return -1;
}
private static String getFullPathName(Property property) {
StringBuilder builder = new StringBuilder();
for (; property != null; property = property.getParent()) {
builder.insert(0, ".").insert(0, property.getName());
}
return builder.toString();
}
public static void moveProperty(List<Property> source, String name, List<Property> destination, int index) {
Property property = extractProperty(source, name);
if (property != null) {
if (index == -1) {
destination.add(property);
}
else {
destination.add(index, property);
}
}
}
@Nullable
public static Property extractProperty(List<Property> properties, String name) {
int size = properties.size();
for (int i = 0; i < size; i++) {
if (name.equals(properties.get(i).getName())) {
return properties.remove(i);
}
}
return null;
}
@Nullable
public Property getSelectionProperty() {
int selectedRow = getSelectedRow();
if (selectedRow >= 0 && selectedRow < myProperties.size()) {
return myProperties.get(selectedRow);
}
return null;
}
@Nullable
private PropertiesContainer getCurrentComponent() {
return myContainers.size() == 1 ? myContainers.get(0) : null;
}
private List<Property> getChildren(Property property) {
return property.getChildren(getCurrentComponent());
}
private List<Property> getFilterChildren(Property property) {
List<Property> properties = new ArrayList<Property>(getChildren(property));
for (Iterator<Property> I = properties.iterator(); I.hasNext(); ) {
Property child = I.next();
if (child.isExpert() && !myShowExpertProperties) {
I.remove();
}
}
return properties;
}
public boolean isDefault(Property property) throws Exception {
for (PropertiesContainer component : myContainers) {
if (!property.isDefaultRecursively(component)) {
return false;
}
}
return true;
}
@Nullable
protected final Object getValue(Property property) throws Exception {
int size = myContainers.size();
if (size == 0) {
return null;
}
Object value = property.getValue(myContainers.get(0));
for (int i = 1; i < size; i++) {
if (!Comparing.equal(value, property.getValue(myContainers.get(i)))) {
return null;
}
}
return value;
}
private boolean isExpanded(Property property) {
return myExpandedProperties.contains(property.getPath());
}
private void collapse(int rowIndex) {
int selectedRow = getSelectedRow();
Property property = myProperties.get(rowIndex);
int size = collapse(property, rowIndex + 1);
LOG.assertTrue(size > 0);
myModel.fireTableDataChanged();
if (selectedRow != -1) {
if (selectedRow > rowIndex) {
selectedRow -= size;
}
getSelectionModel().setSelectionInterval(selectedRow, selectedRow);
}
}
private int collapse(Property property, int startIndex) {
int totalSize = 0;
if (myExpandedProperties.remove(property.getPath())) {
int size = getFilterChildren(property).size();
totalSize += size;
for (int i = 0; i < size; i++) {
totalSize += collapse(myProperties.remove(startIndex), startIndex);
}
}
return totalSize;
}
private void expand(int rowIndex) {
int selectedRow = getSelectedRow();
Property property = myProperties.get(rowIndex);
String path = property.getPath();
if (myExpandedProperties.contains(path)) {
return;
}
myExpandedProperties.add(path);
List<Property> properties = getFilterChildren(property);
myProperties.addAll(rowIndex + 1, properties);
myModel.fireTableDataChanged();
if (selectedRow != -1) {
if (selectedRow > rowIndex) {
selectedRow += properties.size();
}
getSelectionModel().setSelectionInterval(selectedRow, selectedRow);
}
Rectangle rectStart = getCellRect(selectedRow, 0, true);
Rectangle rectEnd = getCellRect(selectedRow + properties.size(), 0, true);
scrollRectToVisible(
new Rectangle(rectStart.x, rectStart.y, rectEnd.x + rectEnd.width - rectStart.x, rectEnd.y + rectEnd.height - rectStart.y));
}
//////////////////////////////////////////////////////////////////////////////////////////
//
//
//
//////////////////////////////////////////////////////////////////////////////////////////
@Override
public void setValueAt(Object aValue, int row, int column) {
Property property = myProperties.get(row);
super.setValueAt(aValue, row, column);
if (property.needRefreshPropertyList()) {
update();
}
repaint();
}
@Override
public TableCellEditor getCellEditor(int row, int column) {
PropertyEditor editor = myProperties.get(row).getEditor();
editor.removePropertyEditorListener(myPropertyEditorListener); // reorder listener (first)
editor.addPropertyEditorListener(myPropertyEditorListener);
myCellEditor.setEditor(editor);
return myCellEditor;
}
/*
* This method is overriden due to bug in the JTree. The problem is that
* JTree does not properly repaint edited cell if the editor is opaque or
* has opaque child components.
*/
public boolean editCellAt(int row, int column, EventObject e) {
boolean result = super.editCellAt(row, column, e);
repaint(getCellRect(row, column, true));
return result;
}
private void startEditing(int index) {
startEditing(index, false);
}
private void startEditing(int index, boolean startedWithKeyboard) {
final PropertyEditor editor = myProperties.get(index).getEditor();
if (editor == null) {
return;
}
editCellAt(index, convertColumnIndexToView(1));
LOG.assertTrue(editorComp != null);
JComponent preferredComponent = editor.getPreferredFocusedComponent();
if (preferredComponent == null) {
preferredComponent = IdeFocusTraversalPolicy.getPreferredFocusedComponent((JComponent)editorComp);
}
if (preferredComponent != null) {
preferredComponent.requestFocusInWindow();
}
if (startedWithKeyboard) {
// waiting for focus is necessary in case, if 'activate' opens dialog. If we don't wait for focus, after the dialog is shown we'll
// end up with the table focused instead of the dialog
IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(new Runnable() {
@Override
public void run() {
editor.activate();
}
});
}
}
private void finishEditing() {
if (editingRow != -1) {
editingStopped(null);
}
}
public void editingStopped(@Nullable ChangeEvent event) {
if (myStoppingEditing) {
return;
}
myStoppingEditing = true;
LOG.assertTrue(isEditing());
LOG.assertTrue(editingRow != -1);
PropertyEditor editor = myProperties.get(editingRow).getEditor();
editor.removePropertyEditorListener(myPropertyEditorListener);
try {
setValueAt(editor.getValue(), editingRow, editingColumn);
}
catch (Exception e) {
showInvalidInput(e);
}
finally {
removeEditor();
myStoppingEditing = false;
}
}
@Override
public void removeEditor() {
super.removeEditor();
updateEditActions();
}
protected void updateEditActions() {
}
private boolean setValueAtRow(int row, final Object newValue) {
final Property property = myProperties.get(row);
final Object[] oldValue = new Object[1];
boolean isNewValue;
try {
oldValue[0] = getValue(property);
isNewValue = !Comparing.equal(oldValue[0], newValue);
if (newValue == null && oldValue[0] instanceof String && ((String)oldValue[0]).length() == 0) {
isNewValue = false;
}
}
catch (Throwable e) {
isNewValue = true;
}
boolean isSetValue = true;
final boolean[] needRefresh = new boolean[1];
if (isNewValue) {
isSetValue = doSetValue(new ThrowableRunnable<Exception>() {
@Override
public void run() throws Exception {
for (PropertiesContainer component : myContainers) {
property.setValue(component, newValue);
needRefresh[0] |= property.needRefreshPropertyList(component, oldValue[0], newValue);
}
}
});
}
if (isSetValue) {
if (property.needRefreshPropertyList() || needRefresh[0]) {
update(myContainers, null, property.closeEditorDuringRefresh());
}
else {
myModel.fireTableRowsUpdated(row, row);
}
}
return isSetValue;
}
protected abstract boolean doSetValue(ThrowableRunnable<Exception> runnable);
private static void showInvalidInput(Exception e) {
Throwable cause = e.getCause();
String message = cause == null ? e.getMessage() : cause.getMessage();
if (message == null || message.length() == 0) {
message = "No message";
}
Messages.showMessageDialog(MessageFormat.format("Error setting value: {0}", message),
"Invalid Input",
Messages.getErrorIcon());
}
//////////////////////////////////////////////////////////////////////////////////////////
//
//
//
//////////////////////////////////////////////////////////////////////////////////////////
/**
* Reimplementation of LookAndFeel's SelectNextRowAction action.
* Standard implementation isn't smart enough.
*
* @see javax.swing.plaf.basic.BasicTableUI
*/
private class MySelectNextPreviousRowAction extends AbstractAction {
private boolean selectNext;
private MySelectNextPreviousRowAction(boolean selectNext) {
this.selectNext = selectNext;
}
public void actionPerformed(ActionEvent e) {
int rowCount = getRowCount();
LOG.assertTrue(rowCount > 0);
int selectedRow = getSelectedRow();
if (selectedRow == -1) {
selectedRow = 0;
}
else {
if (selectNext) {
selectedRow = Math.min(rowCount - 1, getSelectedRow() + 1);
}
else {
selectedRow = Math.max(0, selectedRow - 1);
}
}
if (isEditing()) {
finishEditing();
getSelectionModel().setSelectionInterval(selectedRow, selectedRow);
scrollRectToVisible(getCellRect(selectedRow, 0, true));
startEditing(selectedRow);
}
else {
getSelectionModel().setSelectionInterval(selectedRow, selectedRow);
scrollRectToVisible(getCellRect(selectedRow, 0, true));
}
}
}
/**
* Reimplementation of LookAndFeel's StartEditingAction action.
* Standard implementation isn't smart enough.
*
* @see javax.swing.plaf.basic.BasicTableUI
*/
private class MyStartEditingAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
int selectedRow = getSelectedRow();
if (selectedRow == -1 || isEditing()) {
return;
}
startEditing(selectedRow, true);
}
}
private class MyEnterAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
int selectedRow = getSelectedRow();
if (isEditing() || selectedRow == -1) {
return;
}
Property property = myProperties.get(selectedRow);
if (!getChildren(property).isEmpty()) {
if (isExpanded(property)) {
collapse(selectedRow);
}
else {
expand(selectedRow);
}
}
else {
startEditing(selectedRow, true);
}
}
}
private class MyExpandCurrentAction extends AbstractAction {
private final boolean myExpand;
private final boolean mySelect;
public MyExpandCurrentAction(boolean expand, boolean select) {
myExpand = expand;
mySelect = select;
}
public void actionPerformed(ActionEvent e) {
int selectedRow = getSelectedRow();
if (isEditing() || selectedRow == -1) {
return;
}
Property property = myProperties.get(selectedRow);
List<Property> children = getChildren(property);
if (!children.isEmpty()) {
if (myExpand) {
if (!isExpanded(property)) {
expand(selectedRow);
}
else if (mySelect) {
restoreSelection(children.get(0));
}
}
else if (isExpanded(property)) {
collapse(selectedRow);
}
else if (mySelect) {
Property parent = property.getParent();
if (parent != null) {
restoreSelection(parent);
}
}
}
else if (!myExpand && mySelect) {
Property parent = property.getParent();
if (parent != null) {
restoreSelection(parent);
}
}
}
}
private class MyRestoreDefaultAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
restoreDefaultValue();
}
}
private class MouseTableListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
int row = rowAtPoint(e.getPoint());
if (row == -1) {
return;
}
Property property = myProperties.get(row);
if (getChildren(property).isEmpty()) return;
Icon icon = UIUtil.getTreeNodeIcon(false, true, true);
Rectangle rect = getCellRect(row, convertColumnIndexToView(0), false);
int indent = getBeforeIconAndAfterIndents(property, icon).first;
if (e.getX() < rect.x + indent ||
e.getX() > rect.x + indent + icon.getIconWidth() ||
e.getY() < rect.y ||
e.getY() > rect.y + rect.height) {
return;
}
// TODO: disallow selection for this row
if (isExpanded(property)) {
collapse(row);
}
else {
expand(row);
}
}
}
private class PropertyTableModel extends AbstractTableModel {
@Override
public int getColumnCount() {
return myColumnNames.length;
}
@Override
public String getColumnName(int column) {
return myColumnNames[column];
}
public boolean isCellEditable(int row, int column) {
return column == 1 && myProperties.get(row).isEditable(getCurrentComponent());
}
@Override
public int getRowCount() {
return myProperties.size();
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
return myProperties.get(rowIndex);
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
setValueAtRow(rowIndex, aValue);
}
}
private static int getDepth(@NotNull Property property) {
int result = 0;
for (Property each = property.getParent(); each != null; each = each.getParent(), result++) {
// empty
}
return result;
}
@NotNull
private static Couple<Integer> getBeforeIconAndAfterIndents(@NotNull Property property, @NotNull Icon icon) {
int nodeIndent = UIUtil.getTreeLeftChildIndent() + UIUtil.getTreeRightChildIndent();
int beforeIcon = nodeIndent * getDepth(property);
int leftIconOffset = Math.max(0, UIUtil.getTreeLeftChildIndent() - (icon.getIconWidth() / 2));
beforeIcon += leftIconOffset;
int afterIcon = Math.max(0, nodeIndent - leftIconOffset - icon.getIconWidth());
return Couple.of(beforeIcon, afterIcon);
}
private class PropertyCellEditorListener implements PropertyEditorListener {
@Override
public void valueCommitted(PropertyEditor source, boolean continueEditing, boolean closeEditorOnError) {
if (isEditing()) {
Object value;
TableCellEditor tableCellEditor = cellEditor;
try {
value = tableCellEditor.getCellEditorValue();
}
catch (Exception e) {
showInvalidInput(e);
return;
}
if (setValueAtRow(editingRow, value)) {
if (!continueEditing && editingRow != -1) {
PropertyEditor editor = myProperties.get(editingRow).getEditor();
editor.removePropertyEditorListener(myPropertyEditorListener);
removeEditor();
}
}
else if (closeEditorOnError) {
tableCellEditor.cancelCellEditing();
}
}
}
@Override
public void editingCanceled(PropertyEditor source) {
if (isEditing()) {
cellEditor.cancelCellEditing();
}
}
@Override
public void preferredSizeChanged(PropertyEditor source) {
}
}
private class PropertyCellEditor extends AbstractCellEditor implements TableCellEditor {
private PropertyEditor myEditor;
public void setEditor(PropertyEditor editor) {
myEditor = editor;
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
try {
JComponent component = myEditor.getComponent(getCurrentComponent(), getPropertyContext(), getValue((Property)value), null);
if (component instanceof JComboBox) {
ComboBox.registerTableCellEditor((JComboBox)component, this);
}
else if (component instanceof JCheckBox) {
component.putClientProperty("JComponent.sizeVariant", UIUtil.isUnderAquaLookAndFeel() ? "small" : null);
}
return component;
}
catch (Throwable e) {
LOG.debug(e);
SimpleColoredComponent errComponent = new SimpleColoredComponent();
errComponent
.append(MessageFormat.format("Error getting value: {0}", e.getMessage()), SimpleTextAttributes.ERROR_ATTRIBUTES);
return errComponent;
}
finally {
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
updateEditActions();
}
});
}
}
@Override
public Object getCellEditorValue() {
try {
return myEditor.getValue();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static void updateRenderer(JComponent component, boolean selected) {
if (selected) {
component.setForeground(UIUtil.getTableSelectionForeground());
component.setBackground(UIUtil.getTableSelectionBackground());
}
else {
component.setForeground(UIUtil.getTableForeground());
component.setBackground(UIUtil.getTableBackground());
}
}
@NotNull
protected abstract TextAttributesKey getErrorAttributes(@NotNull HighlightSeverity severity);
private class PropertyCellRenderer implements TableCellRenderer {
private final ColoredTableCellRenderer myCellRenderer;
private final ColoredTableCellRenderer myGroupRenderer;
private PropertyCellRenderer() {
myCellRenderer = new MyCellRenderer();
myGroupRenderer = new MyCellRenderer() {
private boolean mySelected;
public boolean myDrawTopLine;
@Override
protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
super.customizeCellRenderer(table, value, selected, hasFocus, row, column);
mySelected = selected;
myDrawTopLine = row > 0;
}
@Override
protected void paintBackground(Graphics2D g, int x, int width, int height) {
if (mySelected) {
super.paintBackground(g, x, width, height);
}
else {
UIUtil.drawHeader(g, x, width, height, true, myDrawTopLine);
}
}
};
}
@Override
public Component getTableCellRendererComponent(JTable table,
Object value,
boolean selected,
boolean cellHasFocus,
int row,
int column) {
column = table.convertColumnIndexToModel(column);
Property property = (Property)value;
Color background = table.getBackground();
Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
boolean tableHasFocus = focusOwner != null && SwingUtilities.isDescendingFrom(focusOwner, table);
ColoredTableCellRenderer renderer = property instanceof GroupProperty ? myGroupRenderer : myCellRenderer;
renderer.getTableCellRendererComponent(table, value, selected, cellHasFocus, row, column);
renderer.setBackground(selected ? UIUtil.getTreeSelectionBackground(tableHasFocus) : background);
if (property instanceof GroupProperty) {
renderer.setIpad(new Insets(0, 5, 0, 0));
if (column == 0) {
renderer.append(property.getName());
}
return renderer;
}
boolean isDefault = true;
try {
for (PropertiesContainer container : myContainers) {
if (!property.showAsDefault(container)) {
isDefault = false;
break;
}
}
}
catch (Exception e) {
LOG.debug(e);
}
renderer.clear();
if (column == 0) {
SimpleTextAttributes attr = SimpleTextAttributes.REGULAR_ATTRIBUTES;
if (!selected && !isDefault) {
attr = attr.derive(-1, FileStatus.MODIFIED.getColor(), null, null);
}
if (property.isImportant()) {
attr = attr.derive(attr.getStyle() | SimpleTextAttributes.STYLE_BOLD, null, null, null);
}
if (property.isExpert()) {
attr = attr.derive(attr.getStyle() | SimpleTextAttributes.STYLE_ITALIC, null, null, null);
}
if (property.isDeprecated()) {
attr = attr.derive(attr.getStyle() | SimpleTextAttributes.STYLE_STRIKEOUT, null, null, null);
}
ErrorInfo errorInfo = getErrorInfoForRow(row);
if (errorInfo != null) {
SimpleTextAttributes template = SimpleTextAttributes.fromTextAttributes(
EditorColorsManager.getInstance().getGlobalScheme().getAttributes(getErrorAttributes(errorInfo.getLevel().getSeverity())));
int style = ((template.getStyle() & SimpleTextAttributes.STYLE_WAVED) != 0 ? SimpleTextAttributes.STYLE_WAVED : 0)
| ((template.getStyle() & SimpleTextAttributes.STYLE_UNDERLINE) != 0 ? SimpleTextAttributes.STYLE_UNDERLINE : 0);
attr = attr.derive(attr.getStyle() | style, template.getFgColor(), null, template.getWaveColor());
}
SearchUtil.appendFragments(mySpeedSearch.getEnteredPrefix(), property.getName(),
attr.getStyle(), attr.getFgColor(), attr.getBgColor(), renderer);
Icon icon = UIUtil.getTreeNodeIcon(isExpanded(property), selected, tableHasFocus);
boolean hasChildren = !getChildren(property).isEmpty();
renderer.setIcon(hasChildren ? icon : null);
Couple<Integer> indents = getBeforeIconAndAfterIndents(property, icon);
int indent = indents.first;
if (hasChildren) {
renderer.setIconTextGap(indents.second);
}
else {
indent += icon.getIconWidth() + indents.second;
}
renderer.setIpad(new Insets(0, indent, 0, 0));
return renderer;
}
else {
try {
PropertyRenderer valueRenderer = property.getRenderer();
JComponent component =
valueRenderer.getComponent(getCurrentComponent(), getPropertyContext(), getValue(property), selected, tableHasFocus);
component.setBackground(selected ? UIUtil.getTreeSelectionBackground(tableHasFocus) : background);
component.setFont(table.getFont());
if (component instanceof JCheckBox) {
component.putClientProperty("JComponent.sizeVariant", UIUtil.isUnderAquaLookAndFeel() ? "small" : null);
}
return component;
}
catch (Exception e) {
LOG.debug(e);
renderer.append(MessageFormat.format("Error getting value: {0}", e.getMessage()), SimpleTextAttributes.ERROR_ATTRIBUTES);
return renderer;
}
}
}
private class MyCellRenderer extends ColoredTableCellRenderer {
protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
setPaintFocusBorder(false);
setFocusBorderAroundIcon(true);
}
}
}
private static class GroupProperty extends Property {
public GroupProperty(@Nullable String name) {
super(null, StringUtil.notNullize(name));
}
@NotNull
@Override
public PropertyRenderer getRenderer() {
return new LabelPropertyRenderer(null);
}
@Override
public PropertyEditor getEditor() {
return null;
}
}
}