org.cordovastudio.editors.designer.propertyTable.PropertyTable.java Source code

Java tutorial

Introduction

Here is the source code for org.cordovastudio.editors.designer.propertyTable.PropertyTable.java

Source

/*
 * Copyright 2000-2012 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 org.cordovastudio.editors.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.PropertyEditor;
import com.intellij.designer.propertyTable.PropertyEditorListener;
import com.intellij.designer.propertyTable.PropertyRenderer;
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.Pair;
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(org.cordovastudio.editors.designer.propertyTable.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() != null ? component.getProperties() : Collections.emptyList();
    }

    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).getEditor() != null;
        }

        @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 Pair<Integer, 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 Pair.create(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);

                Pair<Integer, 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;
        }
    }
}