blob: e4b7fccedbff46257dd944c4f172fea17da43666 [file] [log] [blame]
/*
* 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 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.*;
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.*;
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.*;
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;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.plaf.TableUI;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
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.*;
import java.util.List;
/**
* @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() {
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));
}
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);
}
}
public void addNotify() {
super.addNotify();
LafManager.getInstance().addLafManagerListener(myLafManagerListener);
}
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.
*/
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));
}
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));
}
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;
}
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.
*/
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));
}
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;
}
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")};
}
public int getColumnCount(){
return 2;
}
public String getColumnName(final int column){
return myColumnNames[column];
}
public int getRowCount(){
return myProperties.size();
}
public boolean isCellEditable(final int row, final int column){
return column==1 && myProperties.get(row).getEditor() != null;
}
public Object getValueAt(final int row, final int column){
return myProperties.get(row);
}
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() {
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{
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();
}
}
}
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() {
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() {
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);
}
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;
}
public Object getCellEditorValue(){
try {
return myEditor.getValue();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
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{
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{
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{
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{
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;
}
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);
}
}
}
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));
}
}
}