com.intellij.uiDesigner.propertyInspector.PropertyInspectorTable.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.uiDesigner.propertyInspector.PropertyInspectorTable.java

Source

/*
 * Copyright 2000-2009 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.uiDesigner.propertyInspector;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

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 org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.intellij.codeInsight.daemon.impl.SeverityRegistrar;
import com.intellij.icons.AllIcons;
import com.intellij.ide.ui.LafManager;
import com.intellij.ide.ui.LafManagerListener;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataKey;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PropertyUtil;
import com.intellij.ui.ColoredTableCellRenderer;
import com.intellij.ui.DoubleClickListener;
import com.intellij.ui.Gray;
import com.intellij.ui.JBColor;
import com.intellij.ui.PopupHandler;
import com.intellij.ui.SimpleColoredComponent;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.TableUtil;
import com.intellij.uiDesigner.ErrorAnalyzer;
import com.intellij.uiDesigner.ErrorInfo;
import com.intellij.uiDesigner.Properties;
import com.intellij.uiDesigner.UIDesignerBundle;
import com.intellij.uiDesigner.actions.ShowJavadocAction;
import com.intellij.uiDesigner.componentTree.ComponentTree;
import com.intellij.uiDesigner.designSurface.GuiEditor;
import com.intellij.uiDesigner.palette.Palette;
import com.intellij.uiDesigner.propertyInspector.properties.*;
import com.intellij.uiDesigner.radComponents.RadComponent;
import com.intellij.uiDesigner.radComponents.RadContainer;
import com.intellij.uiDesigner.radComponents.RadHSpacer;
import com.intellij.uiDesigner.radComponents.RadRootContainer;
import com.intellij.uiDesigner.radComponents.RadVSpacer;
import com.intellij.util.ui.EmptyIcon;
import com.intellij.util.ui.IndentedIcon;
import com.intellij.util.ui.Table;
import com.intellij.util.ui.UIUtil;
import icons.UIDesignerIcons;

/**
 * @author Anton Katilin
 * @author Vladimir Kondratyev
 */
public final class PropertyInspectorTable extends Table implements DataProvider {
    private static final Logger LOG = Logger
            .getInstance("#com.intellij.uiDesigner.propertyInspector.PropertyInspectorTable");

    public static final DataKey<PropertyInspectorTable> DATA_KEY = DataKey
            .create(PropertyInspectorTable.class.getName());

    private static final Color SYNTETIC_PROPERTY_BACKGROUND = new JBColor(Gray._230,
            UIUtil.getPanelBackground().brighter());
    private static final Color SYNTETIC_SUBPROPERTY_BACKGROUND = new JBColor(Gray._240,
            UIUtil.getPanelBackground().brighter());

    private final ComponentTree myComponentTree;
    private final ArrayList<Property> myProperties;
    private final MyModel myModel;
    private final MyCompositeTableCellRenderer myCellRenderer;
    private final MyCellEditor myCellEditor;
    private GuiEditor myEditor;
    /**
     * This listener gets notifications from current property editor
     */
    private final MyPropertyEditorListener myPropertyEditorListener;
    /**
     * Updates UIs of synthetic properties
     */
    private final MyLafManagerListener myLafManagerListener;
    /**
     * This is property exists in this map then it's expanded.
     * It means that its children is visible.
     */
    private final HashSet<String> myExpandedProperties;
    /**
     * Component to be edited
     */
    @NotNull
    private final List<RadComponent> mySelection = new ArrayList<RadComponent>();
    /**
     * If true then inspector will show "expert" properties
     */
    private boolean myShowExpertProperties;

    private final Map<HighlightSeverity, SimpleTextAttributes> myHighlightAttributes = new HashMap<HighlightSeverity, SimpleTextAttributes>();
    private final Map<HighlightSeverity, SimpleTextAttributes> myModifiedHighlightAttributes = new HashMap<HighlightSeverity, SimpleTextAttributes>();

    private final ClassToBindProperty myClassToBindProperty;
    private final BindingProperty myBindingProperty;
    private final BorderProperty myBorderProperty;
    private final LayoutManagerProperty myLayoutManagerProperty = new LayoutManagerProperty();
    private final ButtonGroupProperty myButtonGroupProperty = new ButtonGroupProperty();

    private boolean myInsideSynch;
    private boolean myStoppingEditing;
    private final Project myProject;

    @NonNls
    private static final String ourHelpID = "guiDesigner.uiTour.inspector";

    PropertyInspectorTable(Project project, @NotNull final ComponentTree componentTree) {
        myProject = project;
        myClassToBindProperty = new ClassToBindProperty(project);
        myBindingProperty = new BindingProperty(project);
        myBorderProperty = new BorderProperty(project);

        myPropertyEditorListener = new MyPropertyEditorListener();
        myLafManagerListener = new MyLafManagerListener();
        myComponentTree = componentTree;
        myProperties = new ArrayList<Property>();
        myExpandedProperties = new HashSet<String>();
        myModel = new MyModel();
        setModel(myModel);
        setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

        myCellRenderer = new MyCompositeTableCellRenderer();
        myCellEditor = new MyCellEditor();

        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(final MouseEvent e) {
                final int row = rowAtPoint(e.getPoint());
                if (row == -1) {
                    return;
                }
                final Property property = myProperties.get(row);
                int indent = getPropertyIndent(property) * 11;
                final Rectangle rect = getCellRect(row, convertColumnIndexToView(0), false);
                if (e.getX() < rect.x + indent || e.getX() > rect.x + 9 + indent || e.getY() < rect.y
                        || e.getY() > rect.y + rect.height) {
                    return;
                }

                final Property[] children = getPropChildren(property);
                if (children.length == 0) {
                    return;
                }

                if (isPropertyExpanded(property, property.getParent())) {
                    collapseProperty(row);
                } else {
                    expandProperty(row);
                }
            }
        });

        new DoubleClickListener() {
            @Override
            protected boolean onDoubleClick(MouseEvent e) {
                int row = rowAtPoint(e.getPoint());
                int column = columnAtPoint(e.getPoint());
                if (row >= 0 && column == 0) {
                    final Property property = myProperties.get(row);
                    if (getPropChildren(property).length == 0) {
                        startEditing(row);
                        return true;
                    }
                }
                return false;
            }
        }.installOn(this);

        final AnAction quickJavadocAction = ActionManager.getInstance().getAction(IdeActions.ACTION_QUICK_JAVADOC);
        new ShowJavadocAction().registerCustomShortcutSet(quickJavadocAction.getShortcutSet(), this);

        // Popup menu
        PopupHandler.installPopupHandler(this,
                (ActionGroup) ActionManager.getInstance()
                        .getAction(IdeActions.GROUP_GUI_DESIGNER_PROPERTY_INSPECTOR_POPUP),
                ActionPlaces.GUI_DESIGNER_PROPERTY_INSPECTOR_POPUP, ActionManager.getInstance());
    }

    public void setEditor(final GuiEditor editor) {
        finishEditing();
        myEditor = editor;
        if (myEditor == null) {
            mySelection.clear();
            myProperties.clear();
            myModel.fireTableDataChanged();
        }
    }

    /**
     * @return currently selected {@link IntrospectedProperty} or <code>null</code>
     * if nothing selected or synthetic property is selected.
     */
    @Nullable
    public IntrospectedProperty getSelectedIntrospectedProperty() {
        Property property = getSelectedProperty();
        if (property == null || !(property instanceof IntrospectedProperty)) {
            return null;
        }

        return (IntrospectedProperty) property;
    }

    @Nullable
    public Property getSelectedProperty() {
        final int selectedRow = getSelectedRow();
        if (selectedRow < 0 || selectedRow >= getRowCount()) {
            return null;
        }

        return myProperties.get(selectedRow);
    }

    /**
     * @return {@link PsiClass} of the component which properties are displayed inside the inspector
     */
    public PsiClass getComponentClass() {
        final Module module = myEditor.getModule();

        if (mySelection.size() == 0) {
            return null;
        }
        String className = mySelection.get(0).getComponentClassName();
        for (int i = 1; i < mySelection.size(); i++) {
            if (!Comparing.equal(mySelection.get(i).getComponentClassName(), className)) {
                return null;
            }
        }

        return JavaPsiFacade.getInstance(module.getProject()).findClass(className,
                GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module));
    }

    @Override
    public Object getData(final String dataId) {
        if (getClass().getName().equals(dataId)) {
            return this;
        } else if (CommonDataKeys.PSI_ELEMENT.is(dataId)) {
            final IntrospectedProperty introspectedProperty = getSelectedIntrospectedProperty();
            if (introspectedProperty == null) {
                return null;
            }
            final PsiClass aClass = getComponentClass();
            if (aClass == null) {
                return null;
            }

            final PsiMethod getter = PropertyUtil.findPropertyGetter(aClass, introspectedProperty.getName(), false,
                    true);
            if (getter != null) {
                return getter;
            }

            return PropertyUtil.findPropertySetter(aClass, introspectedProperty.getName(), false, true);
        } else if (CommonDataKeys.PSI_FILE.is(dataId) && myEditor != null) {
            return PsiManager.getInstance(myEditor.getProject()).findFile(myEditor.getFile());
        } else if (GuiEditor.DATA_KEY.is(dataId)) {
            return myEditor;
        } else if (PlatformDataKeys.FILE_EDITOR.is(dataId)) {
            GuiEditor designer = DesignerToolWindowManager.getInstance(myProject).getActiveFormEditor();
            return designer == null ? null : designer.getEditor();
        } else if (PlatformDataKeys.HELP_ID.is(dataId)) {
            return ourHelpID;
        } else {
            return null;
        }
    }

    /**
     * Sets whenther "expert" properties are shown or not
     */
    void setShowExpertProperties(final boolean showExpertProperties) {
        if (myShowExpertProperties == showExpertProperties) {
            return;
        }
        myShowExpertProperties = showExpertProperties;
        if (myEditor != null) {
            synchWithTree(true);
        }
    }

    @Override
    public void addNotify() {
        super.addNotify();
        LafManager.getInstance().addLafManagerListener(myLafManagerListener);
    }

    @Override
    public void removeNotify() {
        LafManager.getInstance().removeLafManagerListener(myLafManagerListener);
        super.removeNotify();
    }

    /**
     * Standard JTable's UI has non convenient keybinding for
     * editing. Therefore we have to replace some standard actions.
     */
    @Override
    public void setUI(final TableUI ui) {
        super.setUI(ui);

        // Customize action and input maps
        @NonNls
        final ActionMap actionMap = getActionMap();
        @NonNls
        final InputMap focusedInputMap = getInputMap(JComponent.WHEN_FOCUSED);
        @NonNls
        final InputMap ancestorInputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

        actionMap.put("selectPreviousRow", new MySelectPreviousRowAction());

        actionMap.put("selectNextRow", new MySelectNextRowAction());

        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("expandCurrent", new MyExpandCurrentAction(true));
        focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), "expandCurrent");
        ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0));

        actionMap.put("collapseCurrent", new MyExpandCurrentAction(false));
        focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), "collapseCurrent");
        ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0));
    }

    @Override
    public void setValueAt(final Object aValue, final int row, final int column) {
        final Property property = myProperties.get(row);
        super.setValueAt(aValue, row, column);
        // We need to repaint whole inspector because change of one property
        // might causes change of another property.
        if (property.needRefreshPropertyList()) {
            synchWithTree(true);
        }
        repaint();
    }

    /**
     * Gets first selected component from ComponentTree and sets it for editing.
     * The method tries to keep selection in the list, so if new component has the property
     * which is already selected then the new value will be
     * also selected. It is very convenient.
     *
     * @param forceSynch if <code>false</code> and selected component in the ComponentTree
     *                   is the same as current component in the PropertyInspector then method does
     *                   nothing such sace. If <code>true</code> then inspector is forced to resynch.
     */
    public void synchWithTree(final boolean forceSynch) {
        if (myInsideSynch) {
            return;
        }
        myInsideSynch = true;
        try {
            RadComponent[] newSelection = myComponentTree.getSelectedComponents();
            if (!forceSynch && mySelection.size() == newSelection.length) {
                boolean anyChanges = false;
                for (RadComponent c : newSelection) {
                    if (!mySelection.contains(c)) {
                        anyChanges = true;
                        break;
                    }
                }
                if (!anyChanges) {
                    return;
                }
            }

            mySelection.clear();
            Collections.addAll(mySelection, newSelection);

            if (isEditing()) {
                cellEditor.stopCellEditing();
            }

            // Store selected property
            final int selectedRow = getSelectedRow();
            Property selectedProperty = null;
            if (selectedRow >= 0 && selectedRow < myProperties.size()) {
                selectedProperty = myProperties.get(selectedRow);
            }

            collectPropertiesForSelection();
            myModel.fireTableDataChanged();

            // Try to restore selection
            final ArrayList<Property> reversePath = new ArrayList<Property>(2);
            while (selectedProperty != null) {
                reversePath.add(selectedProperty);
                selectedProperty = selectedProperty.getParent();
            }
            int indexToSelect = -1;
            for (int i = reversePath.size() - 1; i >= 0; i--) {
                final Property property = reversePath.get(i);
                int index = findPropertyByName(myProperties, property.getName());
                if (index == -1 && indexToSelect != -1) { // try to expand parent and try again
                    expandProperty(indexToSelect);
                    index = findPropertyByName(myProperties, property.getName());
                    if (index != -1) {
                        indexToSelect = index;
                    } else {
                        break;
                    }
                } else {
                    indexToSelect = index;
                }
            }

            if (indexToSelect != -1) {
                getSelectionModel().setSelectionInterval(indexToSelect, indexToSelect);
            } else if (getRowCount() > 0) {
                // Select first row if it's impossible to restore selection
                getSelectionModel().setSelectionInterval(0, 0);
            }
            TableUtil.scrollSelectionToVisible(this);
        } finally {
            myInsideSynch = false;
        }
    }

    private void collectPropertiesForSelection() {
        myProperties.clear();
        if (mySelection.size() > 0) {
            collectProperties(mySelection.get(0), myProperties);

            for (int propIndex = myProperties.size() - 1; propIndex >= 0; propIndex--) {
                if (!myProperties.get(propIndex).appliesToSelection(mySelection)) {
                    myProperties.remove(propIndex);
                }
            }

            for (int i = 1; i < mySelection.size(); i++) {
                ArrayList<Property> otherProperties = new ArrayList<Property>();
                collectProperties(mySelection.get(i), otherProperties);
                for (int propIndex = myProperties.size() - 1; propIndex >= 0; propIndex--) {
                    final Property prop = myProperties.get(propIndex);
                    int otherPropIndex = findPropertyByName(otherProperties, prop.getName());
                    if (otherPropIndex < 0) {
                        myProperties.remove(propIndex);
                        continue;
                    }
                    final Property otherProp = otherProperties.get(otherPropIndex);
                    if (!otherProp.getClass().equals(prop.getClass())) {
                        myProperties.remove(propIndex);
                        continue;
                    }
                    Property[] children = prop.getChildren(mySelection.get(0));
                    Property[] otherChildren = otherProp.getChildren(mySelection.get(i));
                    if (children.length != otherChildren.length) {
                        myProperties.remove(propIndex);
                        continue;
                    }
                    for (int childIndex = 0; childIndex < children.length; childIndex++) {
                        if (!Comparing.equal(children[childIndex].getName(), otherChildren[childIndex].getName())) {
                            myProperties.remove(propIndex);
                            break;
                        }
                    }
                }
            }
        }
    }

    /**
     * @return index of the property with specified <code>name</code>.
     * If there is no such property then the method returns <code>-1</code>.
     */
    private static int findPropertyByName(final ArrayList<Property> properties, final String name) {
        for (int i = properties.size() - 1; i >= 0; i--) {
            final Property property = properties.get(i);
            if (property.getName().equals(name)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Populates result list with the properties available for the specified
     * component
     */
    private void collectProperties(final RadComponent component, final ArrayList<Property> result) {
        if (component instanceof RadRootContainer) {
            addProperty(result, myClassToBindProperty);
        } else {
            if (!(component instanceof RadVSpacer || component instanceof RadHSpacer)) {
                addProperty(result, myBindingProperty);
                addProperty(result, CustomCreateProperty.getInstance(myProject));
            }

            if (component instanceof RadContainer) {
                RadContainer container = (RadContainer) component;
                if (container.getLayoutManager().getName() != null) {
                    addProperty(result, myLayoutManagerProperty);
                }
                addProperty(result, myBorderProperty);

                final Property[] containerProperties = container.getLayoutManager()
                        .getContainerProperties(myProject);
                addApplicableProperties(containerProperties, container, result);
            }

            final RadContainer parent = component.getParent();
            if (parent != null) {
                final Property[] properties = parent.getLayoutManager().getComponentProperties(myProject,
                        component);
                addApplicableProperties(properties, component, result);
            }

            if (component.getDelegee() instanceof AbstractButton && !(component.getDelegee() instanceof JButton)) {
                addProperty(result, myButtonGroupProperty);
            }
            if (!(component instanceof RadVSpacer || component instanceof RadHSpacer)) {
                addProperty(result, ClientPropertiesProperty.getInstance(myProject));
            }

            if (component.hasIntrospectedProperties()) {
                final Class componentClass = component.getComponentClass();
                final IntrospectedProperty[] introspectedProperties = Palette.getInstance(myEditor.getProject())
                        .getIntrospectedProperties(component);
                final Properties properties = Properties.getInstance();
                for (final IntrospectedProperty property : introspectedProperties) {
                    if (!property.appliesTo(component)) {
                        continue;
                    }
                    if (!myShowExpertProperties && properties.isExpertProperty(component.getModule(),
                            componentClass, property.getName()) && !isModifiedForSelection(property)) {
                        continue;
                    }
                    addProperty(result, property);
                }
            }
        }
    }

    private void addApplicableProperties(final Property[] containerProperties, final RadComponent component,
            final ArrayList<Property> result) {
        for (Property prop : containerProperties) {
            //noinspection unchecked
            if (prop.appliesTo(component)) {
                addProperty(result, prop);
            }
        }
    }

    private void addProperty(final ArrayList<Property> result, final Property property) {
        result.add(property);
        if (isPropertyExpanded(property, property.getParent())) {
            for (Property child : getPropChildren(property)) {
                addProperty(result, child);
            }
        }
    }

    private boolean isPropertyExpanded(final Property property, final Property parent) {
        return myExpandedProperties.contains(getDottedName(property));
    }

    private static String getDottedName(final Property property) {
        final Property parent = property.getParent();
        if (parent != null) {
            return parent.getName() + "." + property.getName();
        }
        return property.getName();
    }

    private static int getPropertyIndent(final Property property) {
        final Property parent = property.getParent();
        if (parent != null) {
            return parent.getParent() != null ? 2 : 1;
        }
        return 0;
    }

    private Property[] getPropChildren(final Property property) {
        return property.getChildren(mySelection.get(0));
    }

    @Override
    public TableCellEditor getCellEditor(final int row, final int column) {
        final PropertyEditor editor = myProperties.get(row).getEditor();
        editor.removePropertyEditorListener(myPropertyEditorListener); // we do not need to add listener on every invocation
        editor.addPropertyEditorListener(myPropertyEditorListener);
        myCellEditor.setEditor(editor);
        return myCellEditor;
    }

    @Override
    public TableCellRenderer getCellRenderer(final int row, final int column) {
        return myCellRenderer;
    }

    /*
       * 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.
       */
    @Override
    public boolean editCellAt(final int row, final int column, final EventObject e) {
        final boolean result = super.editCellAt(row, column, e);
        final Rectangle cellRect = getCellRect(row, column, true);
        repaint(cellRect);
        return result;
    }

    /**
     * Starts editing property with the specified <code>index</code>.
     * The method does nothing is property isn't editable.
     */
    private void startEditing(final int index) {
        final Property property = myProperties.get(index);
        final PropertyEditor editor = property.getEditor();
        if (editor == null) {
            return;
        }
        editCellAt(index, convertColumnIndexToView(1));
        LOG.assertTrue(editorComp != null);
        // Now we have to request focus into the editor component
        JComponent prefComponent = editor.getPreferredFocusedComponent((JComponent) editorComp);
        if (prefComponent == null) { // use default policy to find preferred focused component
            prefComponent = IdeFocusTraversalPolicy.getPreferredFocusedComponent((JComponent) editorComp);
        }
        if (prefComponent != null) {
            prefComponent.requestFocusInWindow();
        }
    }

    private void finishEditing() {
        if (editingRow == -1) {
            return;
        }
        editingStopped(new ChangeEvent(cellEditor));
    }

    @Override
    public void editingStopped(final ChangeEvent ignored) {
        LOG.assertTrue(isEditing());
        LOG.assertTrue(editingRow != -1);
        if (myStoppingEditing) {
            return;
        }
        myStoppingEditing = true;
        final Property property = myProperties.get(editingRow);
        final PropertyEditor editor = property.getEditor();
        editor.removePropertyEditorListener(myPropertyEditorListener);
        try {
            if (myEditor != null && !myEditor.isUndoRedoInProgress()) {
                final Object value = editor.getValue();
                setValueAt(value, editingRow, editingColumn);
            }
        } catch (final Exception exc) {
            showInvalidInput(exc);
        } finally {
            removeEditor();
            myStoppingEditing = false;
        }
    }

    private static void showInvalidInput(final Exception exc) {
        final Throwable cause = exc.getCause();
        String message;
        if (cause != null) {
            message = cause.getMessage();
        } else {
            message = exc.getMessage();
        }
        if (message == null || message.length() == 0) {
            message = UIDesignerBundle.message("error.no.message");
        }
        Messages.showMessageDialog(UIDesignerBundle.message("error.setting.value", message),
                UIDesignerBundle.message("title.invalid.input"), Messages.getErrorIcon());
    }

    /**
     * Expands property with the specified index. The method fires event that
     * model changes and keeps currently selected row.
     */
    private void expandProperty(final int index) {
        final int selectedRow = getSelectedRow();

        // Expand property
        final Property property = myProperties.get(index);
        final String dottedName = getDottedName(property);

        // it's possible that property was expanded and we switched to a component which doesn't have this property
        if (myExpandedProperties.contains(dottedName)) {
            return;
        }
        myExpandedProperties.add(dottedName);

        final Property[] children = getPropChildren(property);
        for (int i = 0; i < children.length; i++) {
            myProperties.add(index + i + 1, children[i]);
        }
        myModel.fireTableDataChanged();

        // Restore selected row
        if (selectedRow != -1) {
            getSelectionModel().setSelectionInterval(selectedRow, selectedRow);
        }
    }

    /**
     * Collapse property with the specified index. The method fires event that
     * model changes and keeps currently selected row.
     */
    private void collapseProperty(final int index) {
        final int selectedRow = getSelectedRow();

        // Expand property
        final Property property = myProperties.get(index);
        LOG.assertTrue(isPropertyExpanded(property, property.getParent()));
        myExpandedProperties.remove(getDottedName(property));

        final Property[] children = getPropChildren(property);
        for (int i = 0; i < children.length; i++) {
            myProperties.remove(index + 1);
        }
        myModel.fireTableDataChanged();

        // Restore selected row
        if (selectedRow != -1) {
            getSelectionModel().setSelectionInterval(selectedRow, selectedRow);
        }
    }

    @Nullable
    ErrorInfo getErrorInfoForRow(final int row) {
        LOG.assertTrue(row < myProperties.size());
        if (mySelection.size() != 1) {
            return null;
        }
        RadComponent component = mySelection.get(0);
        final Property property = myProperties.get(row);
        ErrorInfo errorInfo = null;
        if (myClassToBindProperty.equals(property)) {
            errorInfo = (ErrorInfo) component.getClientProperty(ErrorAnalyzer.CLIENT_PROP_CLASS_TO_BIND_ERROR);
        } else if (myBindingProperty.equals(property)) {
            errorInfo = (ErrorInfo) component.getClientProperty(ErrorAnalyzer.CLIENT_PROP_BINDING_ERROR);
        } else {
            //noinspection unchecked
            ArrayList<ErrorInfo> errors = (ArrayList<ErrorInfo>) component
                    .getClientProperty(ErrorAnalyzer.CLIENT_PROP_ERROR_ARRAY);
            if (errors != null) {
                for (ErrorInfo err : errors) {
                    if (property.getName().equals(err.getPropertyName())) {
                        errorInfo = err;
                        break;
                    }
                }
            }
        }
        return errorInfo;
    }

    /**
     * @return first error for the property at the specified row. If component doesn't contain
     * any error then the method returns <code>null</code>.
     */
    @Nullable
    private String getErrorForRow(final int row) {
        LOG.assertTrue(row < myProperties.size());
        final ErrorInfo errorInfo = getErrorInfoForRow(row);
        return errorInfo != null ? errorInfo.myDescription : null;
    }

    @Override
    public String getToolTipText(final MouseEvent e) {
        final int row = rowAtPoint(e.getPoint());
        if (row == -1) {
            return null;
        }
        return getErrorForRow(row);
    }

    private Object getSelectionValue(final Property property) {
        if (mySelection.size() == 0) {
            return null;
        }
        //noinspection unchecked
        Object result = property.getValue(mySelection.get(0));
        for (int i = 1; i < mySelection.size(); i++) {
            Object otherValue = null;
            if (property instanceof IntrospectedProperty) {
                IntrospectedProperty[] props = Palette.getInstance(myProject)
                        .getIntrospectedProperties(mySelection.get(i));
                for (IntrospectedProperty otherProperty : props) {
                    if (otherProperty.getName().equals(property.getName())) {
                        otherValue = otherProperty.getValue(mySelection.get(i));
                        break;
                    }
                }
            } else {
                //noinspection unchecked
                otherValue = property.getValue(mySelection.get(i));
            }
            if (!Comparing.equal(result, otherValue)) {
                return null;
            }
        }
        return result;
    }

    /**
     * @return false if some of the set value operations have failed; true if everything successful
     */
    private boolean setSelectionValue(Property property, Object newValue) {
        if (!setPropValue(property, mySelection.get(0), newValue)) {
            return false;
        }
        for (int i = 1; i < mySelection.size(); i++) {
            if (property instanceof IntrospectedProperty) {
                IntrospectedProperty[] props = Palette.getInstance(myProject)
                        .getIntrospectedProperties(mySelection.get(i));
                for (IntrospectedProperty otherProperty : props) {
                    if (otherProperty.getName().equals(property.getName())) {
                        if (!setPropValue(otherProperty, mySelection.get(i), newValue)) {
                            return false;
                        }
                        break;
                    }
                }
            } else {
                if (!setPropValue(property, mySelection.get(i), newValue)) {
                    return false;
                }
            }
        }
        return true;
    }

    private static boolean setPropValue(final Property property, final RadComponent c, final Object newValue) {
        try {
            //noinspection unchecked
            property.setValue(c, newValue);
        } catch (Throwable e) {
            LOG.debug(e);
            if (e instanceof InvocationTargetException) { // special handling of warapped exceptions
                e = ((InvocationTargetException) e).getTargetException();
            }
            Messages.showMessageDialog(e.getMessage(), UIDesignerBundle.message("title.invalid.input"),
                    Messages.getErrorIcon());
            return false;
        }
        return true;
    }

    public boolean isModifiedForSelection(final Property property) {
        for (RadComponent c : mySelection) {
            //noinspection unchecked
            if (property.isModified(c)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Adapter to TableModel
     */
    private final class MyModel extends AbstractTableModel {
        private final String[] myColumnNames;

        public MyModel() {
            myColumnNames = new String[] { UIDesignerBundle.message("column.property"),
                    UIDesignerBundle.message("column.value") };
        }

        @Override
        public int getColumnCount() {
            return 2;
        }

        @Override
        public String getColumnName(final int column) {
            return myColumnNames[column];
        }

        @Override
        public int getRowCount() {
            return myProperties.size();
        }

        @Override
        public boolean isCellEditable(final int row, final int column) {
            return column == 1 && myProperties.get(row).getEditor() != null;
        }

        @Override
        public Object getValueAt(final int row, final int column) {
            return myProperties.get(row);
        }

        @Override
        public void setValueAt(final Object newValue, final int row, final int column) {
            if (column != 1) {
                throw new IllegalArgumentException("wrong index: " + column);
            }
            setValueAtRow(row, newValue);
        }

        boolean setValueAtRow(final int row, final Object newValue) {
            final Property property = myProperties.get(row);

            // Optimization: do nothing if value doesn't change
            final Object oldValue = getSelectionValue(property);
            boolean retVal = true;
            if (!Comparing.equal(oldValue, newValue)) {
                final GuiEditor editor = myEditor;
                if (!editor.ensureEditable()) {
                    return false;
                }
                final Ref<Boolean> result = new Ref<Boolean>(Boolean.FALSE);
                CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
                    @Override
                    public void run() {
                        result.set(setSelectionValue(property, newValue));

                        editor.refreshAndSave(false);
                    }
                }, UIDesignerBundle.message("command.set.property.value"), null);

                retVal = result.get().booleanValue();
            }
            if (property.needRefreshPropertyList() && retVal) {
                synchWithTree(true);
            }
            return retVal;
        }
    }

    private final class MyPropertyEditorListener extends PropertyEditorAdapter {
        @Override
        public void valueCommitted(final PropertyEditor source, final boolean continueEditing,
                final boolean closeEditorOnError) {
            if (isEditing()) {
                final Object value;
                final TableCellEditor tableCellEditor = cellEditor;
                try {
                    value = tableCellEditor.getCellEditorValue();
                } catch (final Exception exc) {
                    showInvalidInput(exc);
                    return;
                }
                boolean valueAccepted = myModel.setValueAtRow(editingRow, value);
                if (valueAccepted) {
                    if (!continueEditing) {
                        tableCellEditor.stopCellEditing();
                    }
                } else {
                    if (closeEditorOnError) {
                        tableCellEditor.cancelCellEditing();
                    }
                }
            }
        }

        @Override
        public void editingCanceled(final PropertyEditor source) {
            if (isEditing()) {
                cellEditor.cancelCellEditing();
            }
        }
    }

    private final class MyCompositeTableCellRenderer implements TableCellRenderer {
        /**
         * This renderer paints first column with property names
         */
        private final ColoredTableCellRenderer myPropertyNameRenderer;
        private final ColoredTableCellRenderer myErrorRenderer;
        private final Icon myExpandIcon;
        private final Icon myCollapseIcon;
        private final Icon myIndentedExpandIcon;
        private final Icon myIndentedCollapseIcon;
        private final Icon[] myIndentIcons = new Icon[3];

        public MyCompositeTableCellRenderer() {
            myPropertyNameRenderer = new ColoredTableCellRenderer() {
                @Override
                protected void customizeCellRenderer(final JTable table, final Object value, final boolean selected,
                        final boolean hasFocus, final int row, final int column) {
                    // We will append text later in the
                    setPaintFocusBorder(false);
                    setFocusBorderAroundIcon(true);
                }
            };

            myErrorRenderer = new ColoredTableCellRenderer() {
                @Override
                protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus,
                        int row, int column) {
                    setPaintFocusBorder(false);
                }
            };

            myExpandIcon = UIUtil.isUnderDarcula() ? AllIcons.Mac.Tree_white_right_arrow
                    : UIDesignerIcons.ExpandNode;
            myCollapseIcon = UIUtil.isUnderDarcula() ? AllIcons.Mac.Tree_white_down_arrow
                    : UIDesignerIcons.CollapseNode;
            for (int i = 0; i < myIndentIcons.length; i++) {
                myIndentIcons[i] = new EmptyIcon(myExpandIcon.getIconWidth() + 11 * i,
                        myExpandIcon.getIconHeight());
            }
            myIndentedExpandIcon = new IndentedIcon(myExpandIcon, 11);
            myIndentedCollapseIcon = new IndentedIcon(myCollapseIcon, 11);
        }

        @Override
        public Component getTableCellRendererComponent(final JTable table, @NotNull final Object value,
                final boolean selected, final boolean hasFocus, final int row, int column) {
            myPropertyNameRenderer.getTableCellRendererComponent(table, value, selected, hasFocus, row, column);

            column = table.convertColumnIndexToModel(column);
            final Property property = (Property) value;

            final Color background;
            final Property parent = property.getParent();
            if (property instanceof IntrospectedProperty) {
                background = table.getBackground();
            } else {
                // syntetic property
                background = parent == null ? SYNTETIC_PROPERTY_BACKGROUND : SYNTETIC_SUBPROPERTY_BACKGROUND;
            }

            if (!selected) {
                myPropertyNameRenderer.setBackground(background);
            }

            if (column == 0) { // painter for first column
                SimpleTextAttributes attrs = getTextAttributes(row, property);
                myPropertyNameRenderer.append(property.getName(), attrs);

                // 2. Icon
                if (getPropChildren(property).length > 0) {
                    // This is composite property and we have to show +/- sign
                    if (parent != null) {
                        if (isPropertyExpanded(property, parent)) {
                            myPropertyNameRenderer.setIcon(myIndentedCollapseIcon);
                        } else {
                            myPropertyNameRenderer.setIcon(myIndentedExpandIcon);
                        }
                    } else {
                        if (isPropertyExpanded(property, parent)) {
                            myPropertyNameRenderer.setIcon(myCollapseIcon);
                        } else {
                            myPropertyNameRenderer.setIcon(myExpandIcon);
                        }
                    }
                } else {
                    // If property doesn't have children then we have shift its text
                    // to the right
                    myPropertyNameRenderer.setIcon(myIndentIcons[getPropertyIndent(property)]);
                }
            } else if (column == 1) { // painter for second column
                try {
                    final PropertyRenderer renderer = property.getRenderer();
                    //noinspection unchecked
                    final JComponent component = renderer.getComponent(myEditor.getRootContainer(),
                            getSelectionValue(property), selected, hasFocus);
                    if (!selected) {
                        component.setBackground(background);
                    }
                    if (isModifiedForSelection(property)) {
                        component.setFont(table.getFont().deriveFont(Font.BOLD));
                    } else {
                        component.setFont(table.getFont());
                    }

                    if (component instanceof JCheckBox) {
                        component.putClientProperty("JComponent.sizeVariant",
                                UIUtil.isUnderAquaLookAndFeel() ? "small" : null);
                    }

                    return component;
                } catch (Exception ex) {
                    LOG.debug(ex);
                    myErrorRenderer.clear();
                    myErrorRenderer.append(UIDesignerBundle.message("error.getting.value", ex.getMessage()),
                            SimpleTextAttributes.ERROR_ATTRIBUTES);
                    return myErrorRenderer;
                }
            } else {
                throw new IllegalArgumentException("wrong column: " + column);
            }

            if (!selected) {
                myPropertyNameRenderer.setForeground(PropertyInspectorTable.this.getForeground());
                if (property instanceof IntrospectedProperty) {
                    final RadComponent component = mySelection.get(0);
                    final Class componentClass = component.getComponentClass();
                    if (Properties.getInstance().isExpertProperty(component.getModule(), componentClass,
                            property.getName())) {
                        myPropertyNameRenderer.setForeground(Color.LIGHT_GRAY);
                    }
                }
            }

            return myPropertyNameRenderer;
        }

        private SimpleTextAttributes getTextAttributes(final int row, final Property property) {
            // 1. Text
            ErrorInfo errInfo = getErrorInfoForRow(row);

            SimpleTextAttributes result;
            boolean modified;
            try {
                modified = isModifiedForSelection(property);
            } catch (Exception ex) {
                // ignore exceptions here - they'll be reported as red property values
                modified = false;
            }
            if (errInfo == null) {
                result = modified ? SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES
                        : SimpleTextAttributes.REGULAR_ATTRIBUTES;
            } else {
                final HighlightSeverity severity = errInfo.getHighlightDisplayLevel().getSeverity();
                Map<HighlightSeverity, SimpleTextAttributes> cache = modified ? myModifiedHighlightAttributes
                        : myHighlightAttributes;
                result = cache.get(severity);
                if (result == null) {
                    final TextAttributesKey attrKey = SeverityRegistrar.getSeverityRegistrar(myProject)
                            .getHighlightInfoTypeBySeverity(severity).getAttributesKey();
                    TextAttributes textAttrs = EditorColorsManager.getInstance().getGlobalScheme()
                            .getAttributes(attrKey);
                    if (modified) {
                        textAttrs = textAttrs.clone();
                        textAttrs.setFontType(textAttrs.getFontType() | Font.BOLD);
                    }
                    result = SimpleTextAttributes.fromTextAttributes(textAttrs);
                    cache.put(severity, result);
                }
            }

            if (property instanceof IntrospectedProperty) {
                final RadComponent c = mySelection.get(0);
                if (Properties.getInstance().isPropertyDeprecated(c.getModule(), c.getComponentClass(),
                        property.getName())) {
                    return new SimpleTextAttributes(result.getBgColor(), result.getFgColor(), result.getWaveColor(),
                            result.getStyle() | SimpleTextAttributes.STYLE_STRIKEOUT);
                }
            }

            return result;
        }
    }

    /**
     * This is adapter from PropertyEditor to TableCellEditor interface
     */
    private final class MyCellEditor extends AbstractCellEditor implements TableCellEditor {
        private PropertyEditor myEditor;

        public void setEditor(@NotNull final PropertyEditor editor) {
            myEditor = editor;
        }

        @Override
        public Object getCellEditorValue() {
            try {
                return myEditor.getValue();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public Component getTableCellEditorComponent(final JTable table, @NotNull final Object value,
                final boolean isSelected, final int row, final int column) {
            final Property property = (Property) value;
            try {
                //noinspection unchecked
                final JComponent c = myEditor.getComponent(mySelection.get(0), getSelectionValue(property), null);
                if (c instanceof JComboBox) {
                    c.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
                } else if (c instanceof JCheckBox) {
                    c.putClientProperty("JComponent.sizeVariant", UIUtil.isUnderAquaLookAndFeel() ? "small" : null);
                }

                return c;
            } catch (Exception ex) {
                LOG.debug(ex);
                SimpleColoredComponent errComponent = new SimpleColoredComponent();
                errComponent.append(UIDesignerBundle.message("error.getting.value", ex.getMessage()),
                        SimpleTextAttributes.ERROR_ATTRIBUTES);
                return errComponent;
            }
        }
    }

    /**
     * Reimplementation of LookAndFeel's SelectPreviousRowAction action.
     * Standard implementation isn't smart enough.
     *
     * @see javax.swing.plaf.basic.BasicTableUI
     */
    private final class MySelectPreviousRowAction extends AbstractAction {
        @Override
        public void actionPerformed(final ActionEvent e) {
            final int rowCount = getRowCount();
            LOG.assertTrue(rowCount > 0);
            int selectedRow = getSelectedRow();
            if (selectedRow != -1) {
                selectedRow -= 1;
            }
            selectedRow = (selectedRow + rowCount) % rowCount;
            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 SelectNextRowAction action.
     * Standard implementation isn't smart enough.
     *
     * @see javax.swing.plaf.basic.BasicTableUI
     */
    private final class MySelectNextRowAction extends AbstractAction {
        @Override
        public void actionPerformed(final ActionEvent e) {
            final int rowCount = getRowCount();
            LOG.assertTrue(rowCount > 0);
            final int selectedRow = (getSelectedRow() + 1) % rowCount;
            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 final class MyStartEditingAction extends AbstractAction {
        @Override
        public void actionPerformed(final ActionEvent e) {
            final int selectedRow = getSelectedRow();
            if (selectedRow == -1 || isEditing()) {
                return;
            }

            startEditing(selectedRow);
        }
    }

    /**
     * Expands property which has children or start editing atomic
     * property.
     */
    private final class MyEnterAction extends AbstractAction {
        @Override
        public void actionPerformed(final ActionEvent e) {
            final int selectedRow = getSelectedRow();
            if (isEditing() || selectedRow == -1) {
                return;
            }

            final Property property = myProperties.get(selectedRow);
            if (getPropChildren(property).length > 0) {
                if (isPropertyExpanded(property, property.getParent())) {
                    collapseProperty(selectedRow);
                } else {
                    expandProperty(selectedRow);
                }
            } else {
                startEditing(selectedRow);
            }
        }
    }

    private class MyExpandCurrentAction extends AbstractAction {
        private final boolean myExpand;

        public MyExpandCurrentAction(final boolean expand) {
            myExpand = expand;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            final int selectedRow = getSelectedRow();
            if (isEditing() || selectedRow == -1) {
                return;
            }
            final Property property = myProperties.get(selectedRow);
            if (getPropChildren(property).length > 0) {
                if (myExpand) {
                    if (!isPropertyExpanded(property, property.getParent())) {
                        expandProperty(selectedRow);
                    }
                } else {
                    if (isPropertyExpanded(property, property.getParent())) {
                        collapseProperty(selectedRow);
                    }
                }
            }
        }
    }

    /**
     * Updates UI of editors and renderers of all introspected properties
     */
    private final class MyLafManagerListener implements LafManagerListener {
        /**
         * Recursively updates renderer and editor UIs of all synthetic
         * properties.
         */
        private void updateUI(final Property property) {
            final PropertyRenderer renderer = property.getRenderer();
            renderer.updateUI();
            final PropertyEditor editor = property.getEditor();
            if (editor != null) {
                editor.updateUI();
            }
            final Property[] children = getPropChildren(property);
            for (int i = children.length - 1; i >= 0; i--) {
                final Property child = children[i];
                if (!(child instanceof IntrospectedProperty)) {
                    updateUI(child);
                }
            }
        }

        @Override
        public void lookAndFeelChanged(final LafManager source) {
            updateUI(myBorderProperty);
            updateUI(MarginProperty.getInstance(myProject));
            updateUI(HGapProperty.getInstance(myProject));
            updateUI(VGapProperty.getInstance(myProject));
            updateUI(HSizePolicyProperty.getInstance(myProject));
            updateUI(VSizePolicyProperty.getInstance(myProject));
            updateUI(HorzAlignProperty.getInstance(myProject));
            updateUI(VertAlignProperty.getInstance(myProject));
            updateUI(IndentProperty.getInstance(myProject));
            updateUI(UseParentLayoutProperty.getInstance(myProject));
            updateUI(MinimumSizeProperty.getInstance(myProject));
            updateUI(PreferredSizeProperty.getInstance(myProject));
            updateUI(MaximumSizeProperty.getInstance(myProject));
            updateUI(myButtonGroupProperty);
            updateUI(myLayoutManagerProperty);
            updateUI(SameSizeHorizontallyProperty.getInstance(myProject));
            updateUI(SameSizeVerticallyProperty.getInstance(myProject));
            updateUI(CustomCreateProperty.getInstance(myProject));
            updateUI(ClientPropertiesProperty.getInstance(myProject));
        }
    }

}