Java tutorial
/* * 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)); } } }