/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG 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:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.internal.ui.ridgets.swt;
import java.beans.PropertyDescriptor;
import org.osgi.service.log.LogService;
import org.eclipse.core.databinding.beans.BeansObservables;
import org.eclipse.core.databinding.beans.IBeanObservable;
import org.eclipse.core.databinding.beans.PojoObservables;
import org.eclipse.core.databinding.observable.map.IObservableMap;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.equinox.log.Logger;
import org.eclipse.jface.databinding.viewers.ObservableListTreeContentProvider;
import org.eclipse.jface.viewers.IColorProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.events.TreeListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Resource;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.riena.core.Log4r;
import org.eclipse.riena.core.util.ReflectionUtils;
import org.eclipse.riena.core.util.StringUtils;
import org.eclipse.riena.ui.core.resource.IconSize;
import org.eclipse.riena.ui.ridgets.IColumnFormatter;
import org.eclipse.riena.ui.ridgets.swt.AbstractSWTWidgetRidget;
import org.eclipse.riena.ui.swt.lnf.ILnfResource;
import org.eclipse.riena.ui.swt.lnf.ImageLnfResource;
import org.eclipse.riena.ui.swt.lnf.LnfKeyConstants;
import org.eclipse.riena.ui.swt.lnf.LnfManager;
/**
* Label provider that formats the columns of a {@link TreeRidget} or {@link TreeTableRidget}. {@link IColumnFormatter}s can be used to modify the text, image,
* foreground color, background color or font of a particular column.
* <p>
* The appropriate image for a column is computed in the following fashion:
* <p>
* For the tree image (TreeRidget and column 0 in the TreeTableRidget):
* <ul>
* <li>if the column has a formatter, use the image from the formatter, if not null</li>
* <li>if image accessor properties are specified, use the image returned by the property, if not null</li>
* <li>if image accessor properties for leaves are specified, use the image returned by the property, if not null</li>
* <li>otherwise no image is shown</li>
* </ul>
* <p>
* For columns 1-n (TreeTableRidget):
* <ul>
* <li>if the column has a formatter, use the image from the formatter, if not null</li>
* <li>if the column has a boolean or Boolean value, use the default image for boolean values (i.e. checked / unchecked box)</li>
* <li>otherwise no image is shown</li>
* </ul>
* <p>
* The appropriate foreground color for a column is computed in the following fashion:
* <p>
* For column 0 (TreeRidget):
* <ul>
* <li>if an enablement accessor property is specified and the corresponding tree node is disabled, use a gray color</li>
* <li>otherwise use the widget's foreground color</li>
* </ul>
* <p>
* For columns 0-n (TreeTableRidget):
* <ul>
* <li>if the column has a formatter, use the foreground color from the formatter, if not null</li>
* <li>otherwise use the widget's foreground</li>
* </ul>
*/
public final class TreeRidgetLabelProvider extends TableRidgetLabelProvider implements IColorProvider {
private static final Logger LOGGER = Log4r.getLogger(TreeRidgetLabelProvider.class);
private static final UpdateIconsTreeListener LISTENER = new UpdateIconsTreeListener();
private static final String KEY_LABELPROVIDER = "K_TRLP"; //$NON-NLS-1$
public static final String TREE_KIND_KEY = "kind"; //$NON-NLS-1$
public static final String TREE_KIND_NAVIGATION = "navigation"; //$NON-NLS-1$
private final TreeViewer viewer;
private final IObservableMap enablementAttribute;
private final IObservableMap imageAttribute;
private final IObservableMap openImageAttribute;
private final String[] valueAccessors;
/**
* Creates a new instance.
*
* @param viewer
* a non-null {@link TreeViewer} instance
* @param treeElementClass
* the type of the elements in the tree (i.e. for treeRoot and all children).
* @param knownElements
* a non-null set of observable elements. The label provider may track this set to update the tree as necessary - see
* {@link ObservableListTreeContentProvider#getKnownElements()}
* @param valueAccessors
* a non-null; non-empty array of Strings. Each String specifies an accessor for obtaining an Object value from each child object (example
* "value" specifies "getValue()"). The order in the array corresponds to the initial order of the columns, i.e. the 1st accessor will be used
* for column one/the tree, the 2nd for column two, the 3rd for column three and so on
* @param enablementAccessor
* a String specifying an accessor for obtaining a boolean value from each child. The returned value will determine the enabled/disabled state of
* this child. Example: 'enabled' specifies "isEnabled()" or "getEnabled()". The parameter can be {@code null} to enable all children
* @param imageAccessor
* a String specifying an accessor for obtaining a String value, which is the key (or filename) of an icon. (example "icon" specifies
* "getIcon()). This key will be used to obtain an icon for <b>leaves AND closed nodes</b> of the tree. The leafImageAccessor can be null; in
* that case the default icon is used for all leaves and nodes. Note: nodes will only get a custom icon if an openNodeImageAccessor is supplied
* as well (see below).
* @param openNodeImageAccessor
* a String specifying an accessor for obtaining a String value which is the key (or filename) of an icon. (example "icon" specifies "getIcon()"
* ). This key will be used to obtain an icon for <b>open nodes</b> of the tree. The openNodeImageAccessor can be null; in that case the default
* icon is used for all nodes. Note: nodes will only get a custom icon if an imageAccessor is supplied as well (see above).
* @param formatters
* an array of IColumnFormatters; one for each column. Individual array entries may be null, in that case no formatter will be used for that
* column.
*/
public static TreeRidgetLabelProvider createLabelProvider(final TreeViewer viewer, final Class<?> treeElementClass, final IObservableSet knownElements,
final String[] valueAccessors, final String enablementAccessor, final String imageAccessor, final String openNodeImageAccessor,
final IColumnFormatter[] formatters) {
final IObservableMap[] map = createAttributeMap(treeElementClass, knownElements, valueAccessors, enablementAccessor, imageAccessor,
openNodeImageAccessor);
final int numColumns = valueAccessors.length;
return new TreeRidgetLabelProvider(viewer, map, valueAccessors, enablementAccessor, imageAccessor, openNodeImageAccessor, formatters, numColumns);
}
/**
* Create an array of attributes that this label provides will observe. If observing a bean, and the observed attributes change the label provider will
* update the appropriate element.
*/
private static IObservableMap[] createAttributeMap(final Class<?> treeElementClass, final IObservableSet knownElements, final String[] valueAccessors,
final String enablementAccessor, final String imageAccessor, final String openNodeImageAccessor) {
IObservableMap[] result;
final String[] attributes = computeAttributes(valueAccessors, enablementAccessor, imageAccessor, openNodeImageAccessor);
if (AbstractSWTWidgetRidget.isBean(treeElementClass)) {
result = BeansObservables.observeMaps(knownElements, treeElementClass, attributes);
} else {
result = PojoObservables.observeMaps(knownElements, treeElementClass, attributes);
}
return result;
}
@Override
public String getColumnText(final Object element, final int columnIndex) {
//FIXME the current attributeMap does not hold the element and cannot provide a value
final String columnText = super.getColumnText(element, columnIndex);
if (!StringUtils.isGiven(columnText)) {
if (columnIndex <= valueAccessors.length - 1) {
final String str = valueAccessors[columnIndex];
if (str.length() > 1) {
String s = str.substring(0, 1).toUpperCase();
s = s + str.substring(1);
try {
return String.valueOf(ReflectionUtils.invoke(element, "get" + s)); //$NON-NLS-1$
} catch (final RuntimeException ex) {
LOGGER.log(LogService.LOG_WARNING, "Unexpected error when accessing property " + str + " in " + element, ex); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
}
return columnText;
}
private static String[] computeAttributes(final String[] valueAccessors, final String enablementAccessor, final String imageAccessor,
final String openNodeImageAccessor) {
int length = valueAccessors.length;
if (enablementAccessor != null) {
length++;
}
if (imageAccessor != null) {
length++;
}
if (openNodeImageAccessor != null) {
length++;
}
final String[] attributes = new String[Math.max(length, valueAccessors.length)];
System.arraycopy(valueAccessors, 0, attributes, 0, valueAccessors.length);
if (length > valueAccessors.length) {
int index = valueAccessors.length;
if (enablementAccessor != null) {
// add the enablement attribute to the list of observed attributes for the label provider
attributes[index++] = enablementAccessor;
}
if (imageAccessor != null) {
// add the image accessor to the list of observed attributes for the label provider
attributes[index++] = imageAccessor;
}
if (openNodeImageAccessor != null) {
// add the open node image accessor to the list of observed attributes for the label provider
attributes[index++] = openNodeImageAccessor;
}
}
return attributes;
}
private TreeRidgetLabelProvider(final TreeViewer viewer, final IObservableMap[] attributeMap, final String[] valueAccessors,
final String enablementAccessor, final String imageAccessor, final String openNodeImageAccessor, final IColumnFormatter[] formatters,
final int numColumns) {
super(attributeMap, formatters, numColumns);
this.valueAccessors = valueAccessors;
final Tree tree = viewer.getTree();
tree.removeTreeListener(LISTENER);
tree.addTreeListener(LISTENER);
tree.setData(KEY_LABELPROVIDER, this);
enablementAttribute = findAttribute(attributeMap, enablementAccessor);
imageAttribute = findAttribute(attributeMap, imageAccessor);
openImageAttribute = findAttribute(attributeMap, openNodeImageAccessor);
this.viewer = viewer;
}
@Override
public Image getImage(final Object element) {
// TODO [ev] this fails when run without osgi - see Bug 299267
// boolean isNode = viewer.isExpandable(element);
// String result = null;
// if (isNode) {
// boolean isExpanded = viewer.getExpandedState(element);
// return getImageForNode(element, isExpanded);
// }
if (viewer.isExpandable(element)) {
return getImageForNode(element, viewer.getExpandedState(element));
} else {
return getImageForLeaf(element);
}
}
private Image getImageForNode(final Object element, final boolean isExpanded) {
String imageKey = null;
// First try to get the image key from the attributes.
if (imageAttribute != null && openImageAttribute != null) {
imageKey = isExpanded ? (String) openImageAttribute.get(element) : (String) imageAttribute.get(element);
}
// If there are no attributes or no image key specified, get it from the Lnf theme.
if ((imageKey == null) && (isSubModuleNode(element.getClass()))) {
ILnfResource<?> lnfResource = null;
final Tree tree = (Tree) viewer.getControl();
final boolean navigation = TREE_KIND_NAVIGATION.equals(tree.getData(TREE_KIND_KEY));
if (isExpanded) {
lnfResource = navigation ? LnfManager.getLnf().getLnfResource(LnfKeyConstants.SUB_MODULE_TREE_FOLDER_OPEN_ICON)
: LnfManager.getLnf().getLnfResource(LnfKeyConstants.WORKAREA_TREE_FOLDER_OPEN_ICON);
} else {
lnfResource = navigation ? LnfManager.getLnf().getLnfResource(LnfKeyConstants.SUB_MODULE_TREE_FOLDER_CLOSED_ICON)
: LnfManager.getLnf().getLnfResource(LnfKeyConstants.WORKAREA_TREE_FOLDER_CLOSED_ICON);
}
if (lnfResource instanceof ImageLnfResource) {
final ImageLnfResource imageResource = (ImageLnfResource) lnfResource;
imageKey = imageResource.getImagePath();
}
}
// By default load the image in IconSize.NONE ...
IconSize iconSize = IconSize.NONE;
if (isSubModuleNode(element.getClass())) {
// ... unless it's a SubModuleNode, then take the IconSize from the Lnf configuration.
iconSize = (IconSize) LnfManager.getLnf().getSetting(LnfKeyConstants.EMBEDDED_TITLEBAR_ICON_SIZE);
}
// Try to load the image for the identified key ...
Image image = Activator.getSharedImage(imageKey, iconSize);
if (image == null) {
// ... and if that fails load the default image.
imageKey = isExpanded ? SharedImages.IMG_NODE_EXPANDED : SharedImages.IMG_NODE_COLLAPSED;
image = Activator.getSharedImage(imageKey, iconSize);
}
return image;
}
private Image getImageForLeaf(final Object element) {
String imageKey = null;
// First try to get the image key from the attributes.
if (imageAttribute != null) {
imageKey = (String) imageAttribute.get(element);
}
// If there are no attributes or no image key specified, get it from the Lnf theme.
if ((imageKey == null) && (isSubModuleNode(element.getClass()))) {
ILnfResource<? extends Resource> lnfResource = null;
final Tree tree = (Tree) viewer.getControl();
final boolean navigation = TREE_KIND_NAVIGATION.equals(tree.getData(TREE_KIND_KEY));
lnfResource = navigation ? LnfManager.getLnf().getLnfResource(LnfKeyConstants.SUB_MODULE_TREE_DOCUMENT_LEAF_ICON)
: LnfManager.getLnf().getLnfResource(LnfKeyConstants.WORKAREA_TREE_DOCUMENT_LEAF_ICON);
if (lnfResource instanceof ImageLnfResource) {
final ImageLnfResource imageResource = (ImageLnfResource) lnfResource;
imageKey = imageResource.getImagePath();
}
}
// By default load the image in IconSize.NONE ...
IconSize iconSize = IconSize.NONE;
if (isSubModuleNode(element.getClass())) {
// ... unless it's a SubModuleNode, then take the IconSize from the Lnf configuration.
iconSize = (IconSize) LnfManager.getLnf().getSetting(LnfKeyConstants.EMBEDDED_TITLEBAR_ICON_SIZE);
}
// Try to load the image for the identified key ...
Image image = Activator.getSharedImage(imageKey, iconSize);
if (image == null) {
// ... and if that fails load the default image.
image = Activator.getSharedImage(SharedImages.IMG_LEAF, iconSize);
}
return image;
}
@Override
public Image getColumnImage(final Object element, final int columnIndex) {
Image result = null;
if (columnIndex == 0) {
// tree column 0 is special, because it contains the node & leaf icons
// a. use the icon from formatter, if present
// b. if formatter returns null (=don't care) or no formatter present,
// use the standard node / leaf icons
// c. no automatic checkbox (=boolean) icons for column 0
final IColumnFormatter formatter = getFormatter(columnIndex);
if (formatter != null) {
result = (Image) formatter.getImage(element);
}
if (result == null) {
result = getImage(element);
}
} else {
// other columns:
// a. use icon from formatter, if present
// b. use automatic checkbox icons for boolean values
result = super.getColumnImage(element, columnIndex);
}
return result;
}
// IColorProvider methods
/////////////////////////
public Color getBackground(final Object element) {
return null;
}
public Color getForeground(final Object element) {
Color result = null;
if (enablementAttribute != null) {
final Object value = enablementAttribute.get(element);
if (Boolean.FALSE.equals(value)) {
result = viewer.getControl().getDisplay().getSystemColor(SWT.COLOR_GRAY);
}
}
return result;
}
// helping methods
// ////////////////
private IObservableMap findAttribute(final IObservableMap[] attributeMap, final String accessor) {
IObservableMap result = null;
if (accessor != null) {
for (int i = attributeMap.length - 1; result == null && i > -1; i--) {
final IObservableMap attribute = attributeMap[i];
final IBeanObservable beanObservable = (IBeanObservable) attribute;
final PropertyDescriptor pd = beanObservable.getPropertyDescriptor();
final String property = pd != null ? pd.getName() : null;
if (accessor.equals(property)) {
result = attribute;
}
}
}
return result;
}
private boolean isSubModuleNode(final Class<?> elementClass) {
final Class<?>[] interfaces = elementClass.getInterfaces();
for (final Class<?> type : interfaces) {
if (type.getName().equals("org.eclipse.riena.navigation.ISubModuleNode")) { //$NON-NLS-1$
return true;
}
}
if (elementClass.getSuperclass() != null) {
return isSubModuleNode(elementClass.getSuperclass());
}
return false;
}
private void updateNodeImage(final TreeItem item, final boolean isExpanded) {
final Object element = item.getData();
Image image = null;
if (getFormatter(0) != null) {
image = (Image) getFormatter(0).getImage(element);
}
if (image == null) {
image = getImageForNode(element, isExpanded);
}
item.setImage(image);
}
// helping classes
// ////////////////
/**
* This listener is in charge of updating a tree item's icon whenever the item is collapsed or expanded.
*/
private static final class UpdateIconsTreeListener implements TreeListener {
public void treeCollapsed(final TreeEvent e) {
// cannot use treeItem.getExpanded() because it has the old value
updateIcon((TreeItem) e.item, false);
}
public void treeExpanded(final TreeEvent e) {
// cannot use treeItem.getExpanded() because it has the old value
updateIcon((TreeItem) e.item, true);
}
private void updateIcon(final TreeItem item, final boolean isExpanded) {
final TreeRidgetLabelProvider labelProvider = (TreeRidgetLabelProvider) item.getParent().getData(KEY_LABELPROVIDER);
labelProvider.updateNodeImage(item, isExpanded);
}
}
}