| /******************************************************************************* |
| * Copyright (c) 2000, 2006 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Gunnar Wagenknecht - fix for bug 21756 [PropertiesView] property view sorting |
| *******************************************************************************/ |
| |
| package org.eclipse.ui.views.properties; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.commands.common.EventManager; |
| import org.eclipse.jface.viewers.CellEditor; |
| import org.eclipse.jface.viewers.ICellEditorListener; |
| import org.eclipse.jface.viewers.ILabelProvider; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.ui.internal.views.ViewsPlugin; |
| |
| /** |
| * <code>PropertySheetEntry</code> is an implementation of |
| * <code>IPropertySheetEntry</code> which uses <code>IPropertySource</code> |
| * and <code>IPropertyDescriptor</code> to interact with domain model objects. |
| * <p> |
| * Every property sheet entry has a single descriptor (except the root entry |
| * which has none). This descriptor determines what property of its objects it |
| * will display/edit. |
| * </p> |
| * <p> |
| * Entries do not listen for changes in their objects. Since there is no |
| * restriction on properties being independent, a change in one property may |
| * affect other properties. The value of a parent's property may also change. As |
| * a result we are forced to refresh the entire entry tree when a property |
| * changes value. |
| * </p> |
| * |
| * @since 3.0 (was previously internal) |
| */ |
| public class PropertySheetEntry extends EventManager implements |
| IPropertySheetEntry { |
| |
| /** |
| * The values we are displaying/editing. These objects repesent the value of |
| * one of the properties of the values of our parent entry. Except for the |
| * root entry where they represent the input (selected) objects. |
| */ |
| private Object[] values = new Object[0]; |
| |
| /** |
| * The property sources for the values we are displaying/editing. |
| */ |
| private Map sources = new HashMap(0); |
| |
| /** |
| * The value of this entry is defined as the the first object in its value |
| * array or, if that object is an <code>IPropertySource</code>, the value |
| * it returns when sent <code>getEditableValue</code> |
| */ |
| private Object editValue; |
| |
| private PropertySheetEntry parent; |
| |
| private IPropertySourceProvider propertySourceProvider; |
| |
| private IPropertyDescriptor descriptor; |
| |
| private CellEditor editor; |
| |
| private String errorText; |
| |
| private PropertySheetEntry[] childEntries = null; |
| |
| /** |
| * Create the CellEditorListener for this entry. It listens for value |
| * changes in the CellEditor, and cancel and finish requests. |
| */ |
| private ICellEditorListener cellEditorListener = new ICellEditorListener() { |
| public void editorValueChanged(boolean oldValidState, |
| boolean newValidState) { |
| if (!newValidState) { |
| // currently not valid so show an error message |
| setErrorText(editor.getErrorMessage()); |
| } else { |
| // currently valid |
| setErrorText(null); |
| } |
| } |
| |
| public void cancelEditor() { |
| setErrorText(null); |
| } |
| |
| public void applyEditorValue() { |
| PropertySheetEntry.this.applyEditorValue(); |
| } |
| }; |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public void addPropertySheetEntryListener( |
| IPropertySheetEntryListener listener) { |
| addListenerObject(listener); |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public void applyEditorValue() { |
| if (editor == null) { |
| return; |
| } |
| |
| // Check if editor has a valid value |
| if (!editor.isValueValid()) { |
| setErrorText(editor.getErrorMessage()); |
| return; |
| } |
| |
| setErrorText(null); |
| |
| // See if the value changed and if so update |
| Object newValue = editor.getValue(); |
| boolean changed = false; |
| if (values.length > 1) { |
| changed = true; |
| } else if (editValue == null) { |
| if (newValue != null) { |
| changed = true; |
| } |
| } else if (!editValue.equals(newValue)) { |
| changed = true; |
| } |
| |
| // Set the editor value |
| if (changed) { |
| setValue(newValue); |
| } |
| } |
| |
| /** |
| * Return the unsorted intersection of all the |
| * <code>IPropertyDescriptor</code>s for the objects. |
| * |
| * @return List |
| */ |
| private List computeMergedPropertyDescriptors() { |
| if (values.length == 0) { |
| return new ArrayList(0); |
| } |
| |
| IPropertySource firstSource = getPropertySource(values[0]); |
| if (firstSource == null) { |
| return new ArrayList(0); |
| } |
| |
| if (values.length == 1) { |
| return Arrays.asList(firstSource.getPropertyDescriptors()); |
| } |
| |
| // get all descriptors from each object |
| Map[] propertyDescriptorMaps = new Map[values.length]; |
| for (int i = 0; i < values.length; i++) { |
| Object object = values[i]; |
| IPropertySource source = getPropertySource(object); |
| if (source == null) { |
| // if one of the selected items is not a property source |
| // then we show no properties |
| return new ArrayList(0); |
| } |
| // get the property descriptors keyed by id |
| propertyDescriptorMaps[i] = computePropertyDescriptorsFor(source); |
| } |
| |
| // intersect |
| Map intersection = propertyDescriptorMaps[0]; |
| for (int i = 1; i < propertyDescriptorMaps.length; i++) { |
| // get the current ids |
| Object[] ids = intersection.keySet().toArray(); |
| for (int j = 0; j < ids.length; j++) { |
| Object object = propertyDescriptorMaps[i].get(ids[j]); |
| if (object == null || |
| // see if the descriptors (which have the same id) are |
| // compatible |
| !((IPropertyDescriptor) intersection.get(ids[j])) |
| .isCompatibleWith((IPropertyDescriptor) object)) { |
| intersection.remove(ids[j]); |
| } |
| } |
| } |
| |
| // sorting is handled in the PropertySheetViewer, return unsorted (in |
| // the original order) |
| ArrayList result = new ArrayList(intersection.size()); |
| IPropertyDescriptor[] firstDescs = firstSource.getPropertyDescriptors(); |
| for (int i = 0; i < firstDescs.length; i++) { |
| IPropertyDescriptor desc = firstDescs[i]; |
| if (intersection.containsKey(desc.getId())) { |
| result.add(desc); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Returns an map of property descritptors (keyed on id) for the given |
| * property source. |
| * |
| * @param source |
| * a property source for which to obtain descriptors |
| * @return a table of decriptors keyed on their id |
| */ |
| private Map computePropertyDescriptorsFor(IPropertySource source) { |
| IPropertyDescriptor[] descriptors = source.getPropertyDescriptors(); |
| Map result = new HashMap(descriptors.length * 2 + 1); |
| for (int i = 0; i < descriptors.length; i++) { |
| result.put(descriptors[i].getId(), descriptors[i]); |
| } |
| return result; |
| } |
| |
| /** |
| * Create our child entries. |
| */ |
| private void createChildEntries() { |
| // get the current descriptors |
| List descriptors = computeMergedPropertyDescriptors(); |
| |
| // rebuild child entries using old when possible |
| PropertySheetEntry[] newEntries = new PropertySheetEntry[descriptors |
| .size()]; |
| for (int i = 0; i < descriptors.size(); i++) { |
| IPropertyDescriptor d = (IPropertyDescriptor) descriptors.get(i); |
| // create new entry |
| PropertySheetEntry entry = createChildEntry(); |
| entry.setDescriptor(d); |
| entry.setParent(this); |
| entry.setPropertySourceProvider(propertySourceProvider); |
| entry.refreshValues(); |
| newEntries[i] = entry; |
| } |
| // only assign if successful |
| childEntries = newEntries; |
| } |
| |
| /** |
| * Factory method to create a new child <code>PropertySheetEntry</code> |
| * instance. |
| * <p> |
| * Subclasses may overwrite to create new instances of their own class. |
| * </p> |
| * |
| * @return a new <code>PropertySheetEntry</code> instance for the |
| * descriptor passed in |
| * @since 3.1 |
| */ |
| protected PropertySheetEntry createChildEntry() { |
| return new PropertySheetEntry(); |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public void dispose() { |
| if (editor != null) { |
| editor.dispose(); |
| editor = null; |
| } |
| // recursive call to dispose children |
| PropertySheetEntry[] entriesToDispose = childEntries; |
| childEntries = null; |
| if (entriesToDispose != null) { |
| for (int i = 0; i < entriesToDispose.length; i++) { |
| // an error in a property source may cause refreshChildEntries |
| // to fail. Since the Workbench handles such errors we |
| // can be left in a state where a child entry is null. |
| if (entriesToDispose[i] != null) { |
| entriesToDispose[i].dispose(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * The child entries of this entry have changed (children added or removed). |
| * Notify all listeners of the change. |
| */ |
| private void fireChildEntriesChanged() { |
| Object[] array = getListeners(); |
| for (int i = 0; i < array.length; i++) { |
| IPropertySheetEntryListener listener = (IPropertySheetEntryListener) array[i]; |
| listener.childEntriesChanged(this); |
| } |
| } |
| |
| /** |
| * The error message of this entry has changed. Notify all listeners of the |
| * change. |
| */ |
| private void fireErrorMessageChanged() { |
| Object[] array = getListeners(); |
| for (int i = 0; i < array.length; i++) { |
| IPropertySheetEntryListener listener = (IPropertySheetEntryListener) array[i]; |
| listener.errorMessageChanged(this); |
| } |
| } |
| |
| /** |
| * The values of this entry have changed. Notify all listeners of the |
| * change. |
| */ |
| private void fireValueChanged() { |
| Object[] array = getListeners(); |
| for (int i = 0; i < array.length; i++) { |
| IPropertySheetEntryListener listener = (IPropertySheetEntryListener) array[i]; |
| listener.valueChanged(this); |
| } |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public String getCategory() { |
| return descriptor.getCategory(); |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public IPropertySheetEntry[] getChildEntries() { |
| if (childEntries == null) { |
| createChildEntries(); |
| } |
| return childEntries; |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public String getDescription() { |
| return descriptor.getDescription(); |
| } |
| |
| /** |
| * Returns the descriptor for this entry. |
| * |
| * @return the descriptor for this entry |
| * @since 3.1 (was previously private) |
| */ |
| protected IPropertyDescriptor getDescriptor() { |
| return descriptor; |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public String getDisplayName() { |
| return descriptor.getDisplayName(); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.ui.views.properties.IPropertySheetEntry#getEditor(org.eclipse.swt.widgets.Composite) |
| */ |
| public CellEditor getEditor(Composite parent) { |
| |
| if (editor == null) { |
| editor = descriptor.createPropertyEditor(parent); |
| if (editor != null) { |
| editor.addListener(cellEditorListener); |
| } |
| } |
| if (editor != null) { |
| editor.setValue(editValue); |
| setErrorText(editor.getErrorMessage()); |
| } |
| return editor; |
| } |
| |
| /** |
| * Returns the edit value for the object at the given index. |
| * |
| * @param index |
| * the value object index |
| * @return the edit value for the object at the given index |
| */ |
| protected Object getEditValue(int index) { |
| Object value = values[index]; |
| IPropertySource source = getPropertySource(value); |
| if (source != null) { |
| value = source.getEditableValue(); |
| } |
| return value; |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public String getErrorText() { |
| return errorText; |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public String getFilters()[] { |
| return descriptor.getFilterFlags(); |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public Object getHelpContextIds() { |
| return descriptor.getHelpContextIds(); |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public Image getImage() { |
| ILabelProvider provider = descriptor.getLabelProvider(); |
| if (provider == null) { |
| return null; |
| } |
| return provider.getImage(editValue); |
| } |
| |
| /** |
| * Returns the parent of this entry. |
| * |
| * @return the parent entry, or <code>null</code> if it has no parent |
| * @since 3.1 |
| */ |
| protected PropertySheetEntry getParent() { |
| return parent; |
| } |
| |
| /** |
| * Returns an property source for the given object. |
| * |
| * @param object |
| * an object for which to obtain a property source or |
| * <code>null</code> if a property source is not available |
| * @return an property source for the given object |
| * @since 3.1 (was previously private) |
| */ |
| protected IPropertySource getPropertySource(Object object) { |
| if (sources.containsKey(object)) |
| return (IPropertySource) sources.get(object); |
| |
| IPropertySource result = null; |
| IPropertySourceProvider provider = propertySourceProvider; |
| |
| if (provider == null && object != null) { |
| provider = (IPropertySourceProvider) ViewsPlugin.getAdapter(object, |
| IPropertySourceProvider.class, false); |
| } |
| |
| if (provider != null) { |
| result = provider.getPropertySource(object); |
| } else { |
| result = (IPropertySource)ViewsPlugin.getAdapter(object, IPropertySource.class, false); |
| } |
| |
| sources.put(object, result); |
| return result; |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public String getValueAsString() { |
| if (editValue == null) { |
| return "";//$NON-NLS-1$ |
| } |
| ILabelProvider provider = descriptor.getLabelProvider(); |
| if (provider == null) { |
| return editValue.toString(); |
| } |
| String text = provider.getText(editValue); |
| if (text == null) { |
| return "";//$NON-NLS-1$ |
| } |
| return text; |
| } |
| |
| /** |
| * Returns the value objects of this entry. |
| * |
| * @return the value objects of this entry |
| * @since 3.1 (was previously private) |
| */ |
| public Object[] getValues() { |
| return values; |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public boolean hasChildEntries() { |
| if (childEntries != null && childEntries.length > 0) { |
| return true; |
| } |
| // see if we could have entires if we were asked |
| return computeMergedPropertyDescriptors().size() > 0; |
| } |
| |
| /** |
| * Update our child entries. This implementation tries to reuse child |
| * entries if possible (if the id of the new descriptor matches the |
| * descriptor id of the old entry). |
| */ |
| private void refreshChildEntries() { |
| if (childEntries == null) { |
| // no children to refresh |
| return; |
| } |
| |
| // get the current descriptors |
| List descriptors = computeMergedPropertyDescriptors(); |
| |
| // cache old entries by their descriptor id |
| Map entryCache = new HashMap(childEntries.length * 2 + 1); |
| for (int i = 0; i < childEntries.length; i++) { |
| PropertySheetEntry childEntry = childEntries[i]; |
| if (childEntry != null) { |
| entryCache.put(childEntry.getDescriptor().getId(), childEntry); |
| } |
| } |
| |
| // create a list of entries to dispose |
| List entriesToDispose = new ArrayList(Arrays.asList(childEntries)); |
| |
| // clear the old entries |
| this.childEntries = null; |
| |
| // rebuild child entries using old when possible |
| PropertySheetEntry[] newEntries = new PropertySheetEntry[descriptors |
| .size()]; |
| boolean entriesChanged = descriptors.size() != entryCache.size(); |
| for (int i = 0; i < descriptors.size(); i++) { |
| IPropertyDescriptor d = (IPropertyDescriptor) descriptors.get(i); |
| // see if we have an entry matching this descriptor |
| PropertySheetEntry entry = (PropertySheetEntry) entryCache.get(d |
| .getId()); |
| if (entry != null) { |
| // reuse old entry |
| entry.setDescriptor(d); |
| entriesToDispose.remove(entry); |
| } else { |
| // create new entry |
| entry = createChildEntry(); |
| entry.setDescriptor(d); |
| entry.setParent(this); |
| entry.setPropertySourceProvider(propertySourceProvider); |
| entriesChanged = true; |
| } |
| entry.refreshValues(); |
| newEntries[i] = entry; |
| } |
| |
| // only assign if successful |
| this.childEntries = newEntries; |
| |
| if (entriesChanged) { |
| fireChildEntriesChanged(); |
| } |
| |
| // Dispose of entries which are no longer needed |
| for (int i = 0; i < entriesToDispose.size(); i++) { |
| ((IPropertySheetEntry) entriesToDispose.get(i)).dispose(); |
| } |
| } |
| |
| /** |
| * Refresh the entry tree from the root down. |
| * |
| * @since 3.1 (was previously private) |
| */ |
| protected void refreshFromRoot() { |
| if (parent == null) { |
| refreshChildEntries(); |
| } else { |
| parent.refreshFromRoot(); |
| } |
| } |
| |
| /** |
| * Update our value objects. We ask our parent for the property values based |
| * on our descriptor. |
| */ |
| private void refreshValues() { |
| // get our parent's value objects |
| Object[] currentSources = parent.getValues(); |
| |
| // loop through the objects getting our property value from each |
| Object[] newValues = new Object[currentSources.length]; |
| for (int i = 0; i < currentSources.length; i++) { |
| IPropertySource source = parent |
| .getPropertySource(currentSources[i]); |
| newValues[i] = source.getPropertyValue(descriptor.getId()); |
| } |
| |
| // set our new values |
| setValues(newValues); |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public void removePropertySheetEntryListener( |
| IPropertySheetEntryListener listener) { |
| removeListenerObject(listener); |
| } |
| |
| /* |
| * (non-Javadoc) Method declared on IPropertySheetEntry. |
| */ |
| public void resetPropertyValue() { |
| if (parent == null) { |
| // root does not have a default value |
| return; |
| } |
| |
| // Use our parent's values to reset our values. |
| boolean change = false; |
| Object[] objects = parent.getValues(); |
| for (int i = 0; i < objects.length; i++) { |
| IPropertySource source = getPropertySource(objects[i]); |
| if (source.isPropertySet(descriptor.getId())) { |
| // fix for https://bugs.eclipse.org/bugs/show_bug.cgi?id=21756 |
| if (source instanceof IPropertySource2) { |
| IPropertySource2 extendedSource = (IPropertySource2) source; |
| // continue with next if property is not resettable |
| if (!extendedSource |
| .isPropertyResettable(descriptor.getId())) { |
| continue; |
| } |
| } |
| source.resetPropertyValue(descriptor.getId()); |
| change = true; |
| } |
| } |
| if (change) { |
| refreshFromRoot(); |
| } |
| } |
| |
| /** |
| * Set the descriptor. |
| * |
| * @param newDescriptor |
| */ |
| private void setDescriptor(IPropertyDescriptor newDescriptor) { |
| // if our descriptor is changing, we have to get rid |
| // of our current editor if there is one |
| if (descriptor != newDescriptor && editor != null) { |
| editor.dispose(); |
| editor = null; |
| } |
| descriptor = newDescriptor; |
| } |
| |
| /** |
| * Set the error text. This should be set to null when the current value is |
| * valid, otherwise it should be set to a error string |
| */ |
| private void setErrorText(String newErrorText) { |
| errorText = newErrorText; |
| // inform listeners |
| fireErrorMessageChanged(); |
| } |
| |
| /** |
| * Sets the parent of the entry to be propertySheetEntry. |
| * |
| * @param propertySheetEntry |
| */ |
| private void setParent(PropertySheetEntry propertySheetEntry) { |
| parent = propertySheetEntry; |
| } |
| |
| /** |
| * Sets a property source provider for this entry. This provider is used to |
| * obtain an <code>IPropertySource</code> for each of this entries |
| * objects. If no provider is set then a default provider is used. |
| * |
| * @param provider |
| * IPropertySourceProvider |
| */ |
| public void setPropertySourceProvider(IPropertySourceProvider provider) { |
| propertySourceProvider = provider; |
| } |
| |
| /** |
| * Set the value for this entry. |
| * <p> |
| * We set the given value as the value for all our value objects. We then |
| * call our parent to update the property we represent with the given value. |
| * We then trigger a model refresh. |
| * <p> |
| * |
| * @param newValue |
| * the new value |
| */ |
| private void setValue(Object newValue) { |
| // Set the value |
| for (int i = 0; i < values.length; i++) { |
| values[i] = newValue; |
| } |
| |
| // Inform our parent |
| parent.valueChanged(this); |
| |
| // Refresh the model |
| refreshFromRoot(); |
| } |
| |
| /** |
| * The <code>PropertySheetEntry</code> implmentation of this method |
| * declared on<code>IPropertySheetEntry</code> will obtain an editable |
| * value for the given objects and update the child entries. |
| * <p> |
| * Updating the child entries will typically call this method on the child |
| * entries and thus the entire entry tree is updated |
| * </p> |
| * |
| * @param objects |
| * the new values for this entry |
| */ |
| public void setValues(Object[] objects) { |
| values = objects; |
| sources = new HashMap(values.length * 2 + 1); |
| |
| if (values.length == 0) { |
| editValue = null; |
| } else { |
| // set the first value object as the entry's value |
| Object newValue = values[0]; |
| |
| // see if we should convert the value to an editable value |
| IPropertySource source = getPropertySource(newValue); |
| if (source != null) { |
| newValue = source.getEditableValue(); |
| } |
| editValue = newValue; |
| } |
| |
| // update our child entries |
| refreshChildEntries(); |
| |
| // inform listeners that our value changed |
| fireValueChanged(); |
| } |
| |
| /** |
| * The value of the given child entry has changed. Therefore we must set |
| * this change into our value objects. |
| * <p> |
| * We must inform our parent so that it can update its value objects |
| * </p> |
| * <p> |
| * Subclasses may override to set the property value in some custom way. |
| * </p> |
| * |
| * @param child |
| * the child entry that changed its value |
| */ |
| protected void valueChanged(PropertySheetEntry child) { |
| for (int i = 0; i < values.length; i++) { |
| IPropertySource source = getPropertySource(values[i]); |
| source.setPropertyValue(child.getDescriptor().getId(), child |
| .getEditValue(i)); |
| } |
| |
| // inform our parent |
| if (parent != null) { |
| parent.valueChanged(this); |
| } |
| } |
| } |