/* * Copyright (c) 2008 Stiftung Deutsches Elektronen-Synchrotron, * Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY. * * THIS SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "../AS IS" BASIS. * WITHOUT WARRANTY OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR PARTICULAR PURPOSE AND * NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR * THE USE OR OTHER DEALINGS IN THE SOFTWARE. SHOULD THE SOFTWARE PROVE DEFECTIVE * IN ANY RESPECT, THE USER ASSUMES THE COST OF ANY NECESSARY SERVICING, REPAIR OR * CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. * NO USE OF ANY SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. * DESY HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, * OR MODIFICATIONS. * THE FULL LICENSE SPECIFYING FOR THE SOFTWARE THE REDISTRIBUTION, MODIFICATION, * USAGE AND OTHER RIGHTS AND OBLIGATIONS IS INCLUDED WITH THE DISTRIBUTION OF THIS * PROJECT IN THE FILE LICENSE.HTML. IF THE LICENSE IS NOT INCLUDED YOU MAY FIND A COPY * AT HTTP://WWW.DESY.DE/LEGAL/LICENSE.HTM */ package org.csstudio.sds.ui.internal.properties.view; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.csstudio.sds.model.DynamicsDescriptor; import org.csstudio.sds.ui.properties.IPropertyDescriptor; import org.eclipse.core.commands.common.EventManager; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.Platform; 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; /** * <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) * * @author Sven Wende */ 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 dynamics descriptors of the properties we are displaying/editing. */ private DynamicsDescriptor[] _dynamicsDescriptors = new DynamicsDescriptor[0]; private List<Map<String, String>> _aliases = new ArrayList<Map<String,String>>(); private Map<String, String> _alias; /** * The dynamics descriptor of the first property, if several properties are * selected and changed at the same time. */ private DynamicsDescriptor _dynamicsDescriptor; /** * The alias descriptors of the underlying widget model. */ /** * 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; /** * The parent property entry. */ private PropertySheetEntry _parent; /** * The property source provider. */ private IPropertySourceProvider _propertySourceProvider; /** * The property descriptor. */ private IPropertyDescriptor _descriptor; /** * The current cell editor. */ private CellEditor _editor; /** * An error text. */ private String _errorText; /** * Child property entries. */ 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() { @Override public void editorValueChanged(final boolean oldValidState, final boolean newValidState) { if (!newValidState) { // currently not valid so show an error message setErrorText(_editor.getErrorMessage()); } else { // currently valid setErrorText(null); } } @Override public void cancelEditor() { setErrorText(null); } @Override public void applyEditorValue() { PropertySheetEntry.this.applyEditorValue(); } }; /** * {@inheritDoc} */ @Override public final void addPropertySheetEntryListener( final IPropertySheetEntryListener listener) { addListenerObject(listener); } /** * {@inheritDoc} */ @Override public final 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 */ @SuppressWarnings("unchecked") 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 */ @SuppressWarnings("unchecked") private Map computePropertyDescriptorsFor(final 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(); entry.refreshDynamicsDescriptors(); entry.refreshAliases(); newEntries[i] = entry; } // only assign if successful _childEntries = newEntries; } /** * Update our dynamics descriptors. */ private void refreshDynamicsDescriptors() { // get our parent's value objects Object[] currentSources = _parent.getValues(); // loop through the objects getting our property value from each DynamicsDescriptor[] newDynamicsDescriptors = new DynamicsDescriptor[currentSources.length]; for (int i = 0; i < currentSources.length; i++) { IPropertySource source = _parent .getPropertySource(currentSources[i]); newDynamicsDescriptors[i] = source .getDynamicsDescriptor(_descriptor.getId()); } // set our new values setDynamicsDescriptors(newDynamicsDescriptors); } private void refreshAliases() { // get our parent's value objects Object[] currentSources = _parent.getValues(); // loop through the objects getting our property value from each List<Map<String, String>> newAliases = new ArrayList<Map<String,String>>(); for (int i = 0; i < currentSources.length; i++) { IPropertySource source = _parent .getPropertySource(currentSources[i]); newAliases.add(source.getAliases()); } // set our new values setAliases(newAliases); } /** * 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(); } /** * {@inheritDoc} */ @Override 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); } } /** * {@inheritDoc} */ @Override public final String getCategory() { return _descriptor.getCategory(); } /** * {@inheritDoc} */ @Override public final IPropertySheetEntry[] getChildEntries() { if (_childEntries == null) { createChildEntries(); } return _childEntries; } /** * {@inheritDoc} */ @Override public final String getDescription() { return _descriptor.getDescription(); } /** * Returns the descriptor for this entry. * * @return the descriptor for this entry * @since 3.1 (was previously private) */ protected final IPropertyDescriptor getDescriptor() { return _descriptor; } /** * {@inheritDoc} */ @Override public final String getDisplayName() { return _descriptor.getDisplayName(); } /** * {@inheritDoc} */ @Override public final CellEditor getEditor(final 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 final Object getEditValue(final int index) { Object value = _values[index]; IPropertySource source = getPropertySource(value); if (source != null) { value = source.getEditableValue(); } return value; } /** * {@inheritDoc} */ @Override public final String getErrorText() { return _errorText; } /** * {@inheritDoc} */ @Override public final String getFilters()[] { return _descriptor.getFilterFlags(); } /** * {@inheritDoc} */ @Override public final Object getHelpContextIds() { return _descriptor.getHelpContextIds(); } /** * {@inheritDoc} */ @Override public final 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 final 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) */ @SuppressWarnings("unchecked") protected final IPropertySource getPropertySource(final Object object) { if (_sources.containsKey(object)) { return (IPropertySource) _sources.get(object); } IPropertySource result = null; IPropertySourceProvider provider = _propertySourceProvider; if (provider == null && object != null) { provider = (IPropertySourceProvider) Platform.getAdapterManager() .getAdapter(object, IPropertySourceProvider.class); } if (provider != null) { result = provider.getPropertySource(object); } else if (object instanceof IPropertySource) { result = (IPropertySource) object; } else if (object instanceof IAdaptable) { result = (IPropertySource) ((IAdaptable) object) .getAdapter(IPropertySource.class); } else { if (object != null) { result = (IPropertySource) Platform.getAdapterManager() .getAdapter(object, IPropertySource.class); } } _sources.put(object, result); return result; } /** * {@inheritDoc} */ @Override public final 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) */ @Override public final Object[] getValues() { return _values; } /** * {@inheritDoc} */ @Override public final 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). */ @SuppressWarnings("unchecked") 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(); entry.refreshDynamicsDescriptors(); entry.refreshAliases(); 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 final 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); } /** * {@inheritDoc} */ @Override public final void removePropertySheetEntryListener( final IPropertySheetEntryListener listener) { removeListenerObject(listener); } /** * {@inheritDoc} */ @Override 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 * the descriptor */ private void setDescriptor(final 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. * * @param newErrorText * the new error text * */ protected final void setErrorText(final String newErrorText) { _errorText = newErrorText; // inform listeners fireErrorMessageChanged(); } /** * Sets the parent of the entry to be propertySheetEntry. * * @param propertySheetEntry * the parent entry */ private void setParent(final 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 final void setPropertySourceProvider( final 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(final 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> implementation 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 */ @Override public final void setValues(final 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(final 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); } } /** * {@inheritDoc} */ @Override public final boolean isDynamicallySampled() { if (_editValue == null) { return false; } IPropertySource propertySource = _parent.getPropertySource(_parent .getValues()[_parent.getValues().length - 1]); if (propertySource != null) { return propertySource .getDynamicsDescriptor(getDescriptor().getId()) != null; } return false; } /** * {@inheritDoc} */ @Override public final DynamicAspectsWizard getDynamicsDescriptionConfigurationWizard() { return new DynamicAspectsWizard(_dynamicsDescriptor, _alias, _descriptor, getValues()[0]); } /** * {@inheritDoc} */ public final void setAliases( final List<Map<String, String>> aliasesList) { _aliases = aliasesList; if (aliasesList.size() == 0) { _alias = null; } else { // set the first value object as the entry's value _alias = _aliases.get(0); } // update our child entries refreshChildEntries(); // inform listeners that our value changed fireValueChanged(); } /** * {@inheritDoc} */ @Override public final void setDynamicsDescriptors( final DynamicsDescriptor[] dynamicsDescriptors) { _dynamicsDescriptors = dynamicsDescriptors; if (dynamicsDescriptors.length == 0) { _dynamicsDescriptor = null; } else { // set the first value object as the entry's value DynamicsDescriptor newValue = _dynamicsDescriptors[0]; _dynamicsDescriptor = newValue; } // update our child entries refreshChildEntries(); // inform listeners that our value changed fireValueChanged(); } /** * {@inheritDoc} */ @Override public final DynamicsDescriptor[] getDynamicsDescriptors() { return _dynamicsDescriptors; } /** * {@inheritDoc} */ @Override public final void applyDynamicsDescriptor( final DynamicsDescriptor newDynamicsDescriptor) { // See if the value changed and if so update boolean changed = false; // See if the value changed and if so update if (_dynamicsDescriptors.length > 1) { changed = true; } else if (_dynamicsDescriptor == null) { if (newDynamicsDescriptor != null) { changed = true; } } else if (!_dynamicsDescriptor.equals(newDynamicsDescriptor)) { changed = true; } // Set the editor value if (changed) { // Set the new descriptor for (int i = 0; i < _dynamicsDescriptors.length; i++) { _dynamicsDescriptors[i] = newDynamicsDescriptor; } // Inform our parent _parent.dynamicDescriptorChanged(this); // Refresh the model refreshFromRoot(); } } /** * The dynamics descriptor of the specified child entry has changed. * Therefore we must set this change into our value objects. We must inform * our parent so that it can update its value objects * * @param child * the child entry that changed its value */ protected void dynamicDescriptorChanged(final PropertySheetEntry child) { for (int i = 0; i < _values.length; i++) { IPropertySource source = getPropertySource(_values[i]); source.setDynamicsDescriptor(child.getDescriptor().getId(), child .getDynamicsDescriptor(i)); } // inform our parent if (_parent != null) { _parent.dynamicDescriptorChanged(this); } } /** * Gets the dynamics descriptor with the specified index. * * @param index * the index * @return a dynamics descriptor */ protected final DynamicsDescriptor getDynamicsDescriptor(final int index) { DynamicsDescriptor descriptor = _dynamicsDescriptors[index]; return descriptor; } }