org.eclipse.riena.internal.ui.ridgets.swt.TreeRidget.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.riena.internal.ui.ridgets.swt.TreeRidget.java

Source

/*******************************************************************************
 * Copyright (c) 2007, 2013 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.IntrospectionException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

import org.osgi.service.log.LogService;

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateListStrategy;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.beans.BeansObservables;
import org.eclipse.core.databinding.beans.PojoObservables;
import org.eclipse.core.databinding.observable.IObservable;
import org.eclipse.core.databinding.observable.Observables;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.list.ObservableList;
import org.eclipse.core.databinding.observable.map.IMapChangeListener;
import org.eclipse.core.databinding.observable.map.IObservableMap;
import org.eclipse.core.databinding.observable.map.MapChangeEvent;
import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
import org.eclipse.core.databinding.observable.set.ISetChangeListener;
import org.eclipse.core.databinding.observable.set.SetChangeEvent;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.runtime.Assert;
import org.eclipse.equinox.log.Logger;
import org.eclipse.jface.databinding.swt.SWTObservables;
import org.eclipse.jface.databinding.viewers.IViewerObservableList;
import org.eclipse.jface.databinding.viewers.ObservableListTreeContentProvider;
import org.eclipse.jface.databinding.viewers.TreeStructureAdvisor;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;

import org.eclipse.riena.core.Log4r;
import org.eclipse.riena.core.util.ListenerList;
import org.eclipse.riena.core.util.ReflectionUtils;
import org.eclipse.riena.core.util.StringUtils;
import org.eclipse.riena.ui.ridgets.IActionListener;
import org.eclipse.riena.ui.ridgets.IColumnFormatter;
import org.eclipse.riena.ui.ridgets.IMarkableRidget;
import org.eclipse.riena.ui.ridgets.IRidget;
import org.eclipse.riena.ui.ridgets.ISelectableRidget;
import org.eclipse.riena.ui.ridgets.ITreeRidget;
import org.eclipse.riena.ui.ridgets.ITreeTableRidget;
import org.eclipse.riena.ui.ridgets.swt.AbstractSWTWidgetRidget;
import org.eclipse.riena.ui.ridgets.swt.AbstractSelectableRidget;
import org.eclipse.riena.ui.ridgets.swt.MarkerSupport;
import org.eclipse.riena.ui.swt.facades.SWTFacade;

/**
 * Ridget for SWT {@link Tree} widgets.
 */
public class TreeRidget extends AbstractSelectableRidget implements ITreeRidget {

    private static final Listener ITEM_ERASER_AND_PAINTER = SWTFacade.getDefault().createTreeItemEraserAndPainter();
    private static final String PREFIX_IS = "is"; //$NON-NLS-1$

    private final SelectionListener selectionTypeEnforcer;
    private final DoubleClickForwarder doubleClickForwarder;
    private final Queue<ExpansionCommand> expansionStack;
    private ListenerList<IActionListener> doubleClickListeners;

    private DataBindingContext dbc;
    /*
     * Binds the viewer's multiple selection to the multiple selection observable. This binding has to be disposed when the ridget is set to output-only, to
     * avoid updating the model. It has to be recreated when the ridget is set to not-output-only.
     */
    private Binding viewerMSB;

    private TreeViewer viewer;
    /* keeps the last legal selection when in 'output only' mode */
    private TreeItem[] savedSelection;

    /*
     * The original array of elements given as input to the ridget via the #bindToModel method. The ridget however works with the copy (treeRoots) in order to
     * be independent of modification to the original array.
     * 
     * Calling #updateFromModel will synchronize the treeRoots array with the model array.
     */
    private Object[] model;
    private Object[] treeRoots;
    private Class<? extends Object> treeElementClass;
    private String childrenAccessor;
    private String parentAccessor;
    private String[] valueAccessors;
    private String[] columnHeaders;
    private String enablementAccessor;
    private String visibilityAccessor;
    private String imageAccessor;
    private String openNodeImageAccessor;
    private String checkExpandedMethod;
    private boolean showRoots = true;
    private StructuredViewerFilterHolder filterHolder;

    public TreeRidget() {
        selectionTypeEnforcer = new SelectionTypeEnforcer();
        doubleClickForwarder = new DoubleClickForwarder();
        expansionStack = new LinkedList<ExpansionCommand>();
        addPropertyChangeListener(IRidget.PROPERTY_ENABLED, new PropertyChangeListener() {
            public void propertyChange(final PropertyChangeEvent evt) {
                applyEraseListener();
            }
        });
        addPropertyChangeListener(IMarkableRidget.PROPERTY_OUTPUT_ONLY, new PropertyChangeListener() {
            public void propertyChange(final PropertyChangeEvent evt) {
                saveSelection();
                if (isOutputOnly()) {
                    disposeMultipleSelectionBinding();
                } else {
                    createMultipleSelectionBinding();
                }
            }
        });
    }

    @Override
    protected void bindUIControl() {
        final Tree control = getUIControl();
        if (control != null && treeRoots != null) {
            applyColumns(control);
            bindToViewer(control);
            bindToSelection();
            control.addSelectionListener(selectionTypeEnforcer);
            control.addMouseListener(doubleClickForwarder);
            updateExpansionState();
            applyEraseListener();
            applyTableColumnHeaders(control);
            getFilterHolder().activate(viewer);
        }
    }

    @Override
    protected void checkUIControl(final Object uiControl) {
        checkType(uiControl, Tree.class);
    }

    @Override
    protected void unbindUIControl() {
        super.unbindUIControl();
        getFilterHolder().deactivate(viewer);
        if (viewer != null) {
            final Object[] elements = viewer.getExpandedElements();
            final ExpansionCommand cmd = new ExpansionCommand(ExpansionState.RESTORE, elements);
            expansionStack.add(cmd);
        }
        if (dbc != null) {
            disposeMultipleSelectionBinding();
            dbc.dispose();
            dbc = null;
        }
        final Tree control = getUIControl();
        if (control != null) {
            control.removeSelectionListener(selectionTypeEnforcer);
            control.removeMouseListener(doubleClickForwarder);
            final SWTFacade facade = SWTFacade.getDefault();
            facade.removeEraseItemListener(control, ITEM_ERASER_AND_PAINTER);
            facade.removePaintItemListener(control, ITEM_ERASER_AND_PAINTER);
        }
        if (viewer != null) {
            // IMPORTANT: remove the change listeners from the input model.
            // Has to happen after disposing the binding to avoid affecting
            // the selection.
            // See also https://bugs.eclipse.org/243374
            viewer.setInput(null);
        }
        viewer = null;
    }

    @Override
    protected final List<?> getRowObservables() {
        List<?> result = null;
        if (viewer != null) { // have roots and control
            final ObservableListTreeContentProvider cp = (ObservableListTreeContentProvider) viewer
                    .getContentProvider();
            result = new ArrayList<Object>(cp.getKnownElements());
        } else if (treeRoots != null) { // have roots only
            result = collectAllElements();
        }
        return result;
    }

    protected void bindToModel(final Object[] treeRoots, final Class<? extends Object> treeElementClass,
            final String childrenAccessor, final String parentAccessor, final String[] valueAccessors,
            final String[] columnHeaders, final String enablementAccessor, final String visibilityAccessor,
            final String imageAccessor, final String openNodeImageAccessor) {

        this.bindToModel(treeRoots, treeElementClass, childrenAccessor, parentAccessor, valueAccessors,
                columnHeaders, enablementAccessor, visibilityAccessor, imageAccessor, openNodeImageAccessor, null);
    }

    protected void bindToModel(final Object[] treeRoots, final Class<? extends Object> treeElementClass,
            final String childrenAccessor, final String parentAccessor, final String[] valueAccessors,
            final String[] columnHeaders, final String enablementAccessor, final String visibilityAccessor,
            final String imageAccessor, final String openNodeImageAccessor, final String expandedAccessor) {
        Assert.isNotNull(treeRoots);
        Assert.isNotNull(treeElementClass);
        Assert.isNotNull(childrenAccessor);
        Assert.isNotNull(parentAccessor);
        Assert.isNotNull(valueAccessors);
        Assert.isLegal(valueAccessors.length > 0, "valueAccessors must have at least one entry"); //$NON-NLS-1$
        if (columnHeaders != null) {
            final String msg = "Mismatch between number of valueAccessors and columnHeaders"; //$NON-NLS-1$
            Assert.isLegal(valueAccessors.length == columnHeaders.length, msg);
        }

        unbindUIControl();

        this.model = treeRoots;
        this.treeRoots = new Object[model.length];
        System.arraycopy(model, 0, this.treeRoots, 0, this.treeRoots.length);
        this.treeElementClass = treeElementClass;
        this.childrenAccessor = childrenAccessor;
        this.parentAccessor = parentAccessor;
        this.valueAccessors = new String[valueAccessors.length];
        System.arraycopy(valueAccessors, 0, this.valueAccessors, 0, this.valueAccessors.length);
        if (columnHeaders != null) {
            this.columnHeaders = new String[columnHeaders.length];
            System.arraycopy(columnHeaders, 0, this.columnHeaders, 0, this.columnHeaders.length);
        } else {
            this.columnHeaders = null;
        }
        this.enablementAccessor = enablementAccessor;
        this.visibilityAccessor = visibilityAccessor;
        this.imageAccessor = imageAccessor;
        this.openNodeImageAccessor = openNodeImageAccessor;
        if (expandedAccessor != null) {
            this.checkExpandedMethod = PREFIX_IS + StringUtils.capitalize(expandedAccessor);
        }

        initializeExpansionStack();

        bindUIControl();
    }

    private void initializeExpansionStack() {
        expansionStack.clear();
        if (treeRoots.length == 1) {
            addExpansionCommand(treeRoots[0]);
            if (checkExpandedMethod != null) {
                addExpansionCommandsForRootDescendants(treeRoots[0]);
            }
        }
    }

    /**
     * Adds commands for expansion AND collapsion for all descendants of the given root element to the <code>expansionStack</code> .
     * 
     * @param element
     *            root element
     */
    private void addExpansionCommandsForRootDescendants(final Object element) {
        final List<Object> allDescendants = new ArrayList<Object>();
        collectChildren(element, allDescendants);
        for (final Object descendant : allDescendants) {
            if (isLeaf(descendant)) {
                continue;
            }
            if (isExpanded(descendant)) {
                addExpansionCommand(descendant);
            } else {
                addCollapseCommand(descendant);
            }
        }
    }

    private boolean isLeaf(final Object element) {
        final List<?> children = getChildren(element);
        return children.isEmpty();
    }

    private boolean isExpanded(final Object element) {
        return ReflectionUtils.invoke(element, checkExpandedMethod);
    }

    private void addExpansionCommand(final Object element) {
        final ExpansionCommand cmd = new ExpansionCommand(ExpansionState.EXPAND, element);
        expansionStack.add(cmd);
    }

    private void addCollapseCommand(final Object element) {
        final ExpansionCommand cmd = new ExpansionCommand(ExpansionState.COLLAPSE, element);
        expansionStack.add(cmd);
    }

    /**
     * Returns the TreeViewer instance used by this ridget or null.
     */
    protected final TreeViewer getViewer() {
        return viewer;
    }

    /**
     * Returns the column formatters for this ridget. Each entry in the array corresponds to a column (i.e. 0 for the 1st column, 1 for the 2nd, etc). If a
     * column has no formatter associated, the array entry will be null. The array has the length {@code numColumns}.
     * <p>
     * Implementation note: This ridget does not support columns, so this array will be filled with null entries. Subclasses that support column formatters must
     * override to return an appropriate array.
     * 
     * @param numColumns
     *            return the number of columns, an integer >= 0.
     * @return an array of IColumnFormatter, never null
     */
    protected IColumnFormatter[] getColumnFormatters(final int numColumns) {
        return new IColumnFormatter[numColumns];
    }

    /**
     * This method changes the width of the Tree's columns.
     * <p>
     * Does nothing by default. Subclasses implementing {@link ITreeTableRidget} must override.
     * 
     * @param control
     *            the Tree control; never null
     */
    protected void applyColumnWidths(final Tree control) {
        // subclasses should override
    }

    // public methods
    // ///////////////

    @Override
    public Tree getUIControl() {
        return (Tree) super.getUIControl();
    }

    @Override
    public void addDoubleClickListener(final IActionListener listener) {
        Assert.isNotNull(listener, "listener is null"); //$NON-NLS-1$
        if (doubleClickListeners == null) {
            doubleClickListeners = new ListenerList<IActionListener>(IActionListener.class);
        }
        doubleClickListeners.add(listener);
    }

    /**
     * {@inheritDoc}
     * <p>
     * This implementation will try to expand the path to the give option, to ensure that the corresponding tree element exists.
     */
    @Override
    public boolean containsOption(final Object option) {
        reveal(new Object[] { option });
        return super.containsOption(option);
    }

    public void bindToModel(final Object[] treeRoots, final Class<? extends Object> treeElementClass,
            final String childrenAccessor, final String parentAccessor, final String valueAccessor) {
        Assert.isNotNull(valueAccessor);
        final String[] myValueAccessors = new String[] { valueAccessor };
        final String[] noColumnHeaders = null;
        final String noEnablementAccessor = null;
        final String noVisibilityAccessor = null;
        final String noImageAccessor = null;
        final String noOpenNodeImageAccessor = null;
        this.bindToModel(treeRoots, treeElementClass, childrenAccessor, parentAccessor, myValueAccessors,
                noColumnHeaders, noEnablementAccessor, noVisibilityAccessor, noImageAccessor,
                noOpenNodeImageAccessor);
    }

    public void bindToModel(final Object[] treeRoots, final Class<? extends Object> treeElementClass,
            final String childrenAccessor, final String parentAccessor, final String valueAccessor,
            final String enablementAccessor, final String visibilityAccessor) {
        Assert.isNotNull(valueAccessor);
        final String[] myValueAccessors = new String[] { valueAccessor };
        final String[] noColumnHeaders = null;
        final String noImageAccessor = null;
        final String noOpenNodeImageAccessor = null;
        this.bindToModel(treeRoots, treeElementClass, childrenAccessor, parentAccessor, myValueAccessors,
                noColumnHeaders, enablementAccessor, visibilityAccessor, noImageAccessor, noOpenNodeImageAccessor);
    }

    public void bindToModel(final Object[] treeRoots, final Class<? extends Object> treeElementClass,
            final String childrenAccessor, final String parentAccessor, final String valueAccessor,
            final String enablementAccessor, final String visibilityAccessor, final String imageAccessor) {
        Assert.isNotNull(valueAccessor);
        final String[] myValueAccessors = new String[] { valueAccessor };
        final String[] noColumnHeaders = null;
        final String noOpenNodeImageAccessor = null;
        this.bindToModel(treeRoots, treeElementClass, childrenAccessor, parentAccessor, myValueAccessors,
                noColumnHeaders, enablementAccessor, visibilityAccessor, imageAccessor, noOpenNodeImageAccessor);
    }

    public void bindToModel(final Object[] treeRoots, final Class<? extends Object> treeElementClass,
            final String childrenAccessor, final String parentAccessor, final String valueAccessor,
            final String enablementAccessor, final String visibilityAccessor, final String imageAccessor,
            final String openNodeImageAccessor) {
        Assert.isNotNull(valueAccessor);
        final String[] myValueAccessors = new String[] { valueAccessor };
        final String[] noColumnHeaders = null;
        this.bindToModel(treeRoots, treeElementClass, childrenAccessor, parentAccessor, myValueAccessors,
                noColumnHeaders, enablementAccessor, visibilityAccessor, imageAccessor, openNodeImageAccessor);
    }

    public void bindToModel(final Object[] treeRoots, final Class<? extends Object> treeElementClass,
            final String childrenAccessor, final String parentAccessor, final String valueAccessor,
            final String enablementAccessor, final String visibilityAccessor, final String imageAccessor,
            final String openNodeImageAccessor, final String expandedAccessor) {
        Assert.isNotNull(valueAccessor);
        final String[] myValueAccessors = new String[] { valueAccessor };
        final String[] noColumnHeaders = null;
        this.bindToModel(treeRoots, treeElementClass, childrenAccessor, parentAccessor, myValueAccessors,
                noColumnHeaders, enablementAccessor, visibilityAccessor, imageAccessor, openNodeImageAccessor,
                expandedAccessor);
    }

    public void collapse(final Object element) {
        addCollapseCommand(element);
        final ExpansionCommand cmd = new ExpansionCommand(ExpansionState.COLLAPSE, element);
        expansionStack.add(cmd);
        updateExpansionState();
    }

    public void collapseAll() {
        final ExpansionCommand cmd = new ExpansionCommand(ExpansionState.FULLY_COLLAPSE, null);
        expansionStack.add(cmd);
        updateExpansionState();
    }

    public void expand(final Object element) {
        addExpansionCommand(element);
        updateExpansionState();
    }

    public void expandAll() {
        final ExpansionCommand cmd = new ExpansionCommand(ExpansionState.FULLY_EXPAND, null);
        expansionStack.add(cmd);
        updateExpansionState();
    }

    public boolean getRootsVisible() {
        return showRoots;
    }

    /**
     * Always returns true because mandatory markers do not make sense for this ridget.
     */
    @Override
    public boolean isDisableMandatoryMarker() {
        return true;
    }

    public void refresh(final Object node) {
        if (viewer != null) {
            viewer.refresh(node, true);
        }
    }

    @Override
    public void removeDoubleClickListener(final IActionListener listener) {
        if (doubleClickListeners != null) {
            doubleClickListeners.remove(listener);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * For each selection candidate in the List <tt>newSelection</tt>, this implementation will try to expand the path to the corresponding tree node, to ensure
     * that the corresponding tree element is selectable.
     */
    @Override
    public final void setSelection(final List<?> newSelection) {
        reveal(newSelection.toArray());
        super.setSelection(newSelection);
        saveSelection();
    }

    /**
     * {@inheritDoc}
     * <p>
     * Implementation notes:
     * <ul>
     * <li>If showRoots is false, the children of the first entry in the array of treeRoots will be shown at level-0 of the tree</li>
     * <li>This method must be ivoked before calling bindToModel(...). If changed afterwards it requires a call to bindToModel() or updateFromModel() to take
     * effect.</li>
     * </ul>
     */
    public void setRootsVisible(final boolean showRoots) {
        firePropertyChange(ITreeRidget.PROPERTY_ROOTS_VISIBLE, this.showRoots, this.showRoots = showRoots);
    }

    @Override
    public void updateFromModel() {
        treeRoots = new Object[model.length];
        System.arraycopy(model, 0, treeRoots, 0, treeRoots.length);
        final List<Object> selection = getSelection();
        if (viewer != null) {
            final Object[] expandedElements = viewer.getExpandedElements();
            viewer.getControl().setRedraw(false);
            try {
                // IMPORTANT: next line removes listeners from old model
                viewer.setInput(null);
                if (showRoots) {
                    viewer.setInput(treeRoots);
                } else {
                    final FakeRoot fakeRoot = new FakeRoot(treeRoots.length > 0 ? treeRoots[0] : null,
                            childrenAccessor);
                    viewer.setInput(fakeRoot);
                }
                viewer.setExpandedElements(expandedElements);
                // update column specific formatters
                final TreeRidgetLabelProvider labelProvider = (TreeRidgetLabelProvider) viewer.getLabelProvider();
                final IColumnFormatter[] formatters = getColumnFormatters(labelProvider.getColumnCount());
                labelProvider.setFormatters(formatters);
                // update expanded/collapsed icons
                viewer.refresh();
                viewer.setSelection(new StructuredSelection(selection));
            } finally {
                viewer.getControl().setRedraw(true);
            }
        } else {
            setSelection(selection);
        }
    }

    private void applyColumns(final Tree control) {
        final int columnCount = control.getColumnCount() == 0 ? 1 : control.getColumnCount();
        final int expectedCols = valueAccessors.length;
        if (columnCount != expectedCols) {
            for (final TreeColumn column : control.getColumns()) {
                column.dispose();
            }
            for (int i = 0; i < expectedCols; i++) {
                new TreeColumn(control, SWT.NONE);
            }
            applyColumnWidths(control);
        }
    }

    private void applyEraseListener() {
        if (viewer != null) {
            final Tree control = viewer.getTree();
            final SWTFacade facade = SWTFacade.getDefault();
            facade.removeEraseItemListener(control, ITEM_ERASER_AND_PAINTER);
            facade.removePaintItemListener(control, ITEM_ERASER_AND_PAINTER);
            if (!isEnabled() && MarkerSupport.isHideDisabledRidgetContent()) {
                facade.addEraseItemListener(control, ITEM_ERASER_AND_PAINTER);
                facade.addPaintItemListener(control, ITEM_ERASER_AND_PAINTER);
            }
        }
    }

    private void applyTableColumnHeaders(final Tree control) {
        final boolean headersVisible = columnHeaders != null;
        control.setHeaderVisible(headersVisible);
        if (headersVisible) {
            final TreeColumn[] columns = control.getColumns();
            for (int i = 0; i < columns.length; i++) {
                String columnHeader = ""; //$NON-NLS-1$
                if (i < columnHeaders.length && columnHeaders[i] != null) {
                    columnHeader = columnHeaders[i];
                }
                columns[i].setText(columnHeader);
            }
        }
    }

    /**
     * Initialize databinding for tree viewer.
     */
    private void bindToViewer(final Tree control) {
        viewer = new SharedControlTreeViewer(control);

        // how to create the content/structure for the tree
        final TreeStructureAdvisor structureAdvisor = createStructureAdvisor();
        final ObservableListTreeContentProvider viewerCP = createContentProvider(structureAdvisor); // one instance per viewer instance

        // refresh icons on addition / removal
        viewer.setContentProvider(viewerCP);
        viewerCP.getKnownElements().addSetChangeListener(new TreeContentChangeListener(viewer, structureAdvisor));

        // labels
        final IColumnFormatter[] formatters = getColumnFormatters(valueAccessors.length);
        final ILabelProvider viewerLP = TreeRidgetLabelProvider.createLabelProvider(viewer, treeElementClass,
                viewerCP.getKnownElements(), valueAccessors, enablementAccessor, imageAccessor,
                openNodeImageAccessor, formatters);
        viewer.setLabelProvider(viewerLP);

        // input
        if (showRoots) {
            viewer.setInput(treeRoots);
        } else {
            final FakeRoot fakeRoot = new FakeRoot(treeRoots.length > 0 ? treeRoots[0] : null, childrenAccessor);
            viewer.setInput(fakeRoot);
        }
        final IObservableMap enablementAttr = createObservableAttribute(viewerCP, enablementAccessor);
        preventDisabledItemSelection(enablementAttr);
        final IObservableMap visibilityAttr = createObservableAttribute(viewerCP, visibilityAccessor);
        monitorVisibility(viewer, structureAdvisor, visibilityAttr);
    }

    /**
     * Initialize databinding related to selection handling (single/multi).
     */
    private void bindToSelection() {
        dbc = new DataBindingContext();
        // viewer to single selection binding
        final IObservableValue viewerSelection = ViewersObservables.observeSingleSelection(viewer);
        dbc.bindValue(viewerSelection, getSingleSelectionObservable(),
                new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE)
                        .setAfterGetValidator(new OutputAwareValidator(this)),
                new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE));
        // viewer to multi selection binding
        viewerMSB = null;
        if (!isOutputOnly()) {
            createMultipleSelectionBinding();
        }
        saveSelection();
    }

    private List<?> collectAllElements() {
        final List<Object> allElements = new ArrayList<Object>();
        for (final Object root : treeRoots) {
            if (root != null) {
                if (showRoots) {
                    allElements.add(root);
                }
                collectChildren(root, allElements);
            }
        }
        return allElements;
    }

    /**
     * Collects all children of the given parent and adds them to the given list.
     * <p>
     * <b>Note</b>: First the leaf of a sub-tree is added, than the parent of the leaf, etc. till the root of the sub-tree.
     * 
     * @param parent
     *            parent element
     * @param result
     *            list of all children
     */
    private void collectChildren(final Object parent, final List<Object> result) {
        final List<?> children = getChildren(parent);
        for (final Object child : children) {
            if (child == null) {
                continue;
            }
            collectChildren(child, result);
            result.add(child);
        }
    }

    private List<?> getChildren(final Object parent) {
        final String methodName = "get" + StringUtils.capitalize(childrenAccessor); //$NON-NLS-1$
        final List<?> children = ReflectionUtils.invoke(parent, methodName);
        return children;
    }

    private ObservableListTreeContentProvider createContentProvider(final TreeStructureAdvisor structureAdvisor) {
        final Realm realm = SWTObservables.getRealm(Display.getDefault());
        // how to obtain an observable list of children from a given object (expansion)
        final IObservableFactory listFactory = new IObservableFactory() {
            public IObservable createObservable(final Object target) {
                if (target instanceof Object[]) {
                    return Observables.staticObservableList(realm, Arrays.asList((Object[]) target));
                }
                Object value;
                if (target instanceof FakeRoot) {
                    value = ((FakeRoot) target).getRoot();
                    if (value == null) {
                        return new ObservableList(Collections.EMPTY_LIST, treeElementClass) {
                            // empty list
                        };
                    }
                } else {
                    value = target;
                }
                if (AbstractSWTWidgetRidget.isBean(treeElementClass)) {
                    return BeansObservables.observeList(realm, value, childrenAccessor, treeElementClass);
                } else {
                    return PojoObservables.observeList(realm, value, childrenAccessor, treeElementClass);
                }
            }
        };

        // how to create the content/structure for the tree
        return new ObservableListTreeContentProvider(listFactory, structureAdvisor);
    }

    private void createMultipleSelectionBinding() {
        if (viewerMSB == null && dbc != null && viewer != null) {
            final StructuredSelection currentSelection = new StructuredSelection(getSelection());
            final IViewerObservableList viewerSelections = ViewersObservables.observeMultiSelection(viewer);
            viewerMSB = dbc.bindList(viewerSelections, getMultiSelectionObservable(),
                    new UpdateListStrategy(UpdateListStrategy.POLICY_UPDATE),
                    new UpdateListStrategy(UpdateListStrategy.POLICY_UPDATE));
            viewer.setSelection(currentSelection);
        }
    }

    private IObservableMap createObservableAttribute(final ObservableListTreeContentProvider viewerCP,
            final String accessor) {
        IObservableMap result = null;
        if (accessor != null) {
            if (AbstractSWTWidgetRidget.isBean(treeElementClass)) {
                result = BeansObservables.observeMap(viewerCP.getKnownElements(), treeElementClass, accessor);
            } else {
                result = PojoObservables.observeMap(viewerCP.getKnownElements(), treeElementClass, accessor);
            }
        }
        return result;
    }

    private TreeStructureAdvisor createStructureAdvisor() {
        // how to get the parent from a given object
        return new GenericTreeStructureAdvisor(parentAccessor, treeElementClass);
    }

    private void disposeMultipleSelectionBinding() {
        if (viewerMSB != null) { // implies dbc != null
            viewerMSB.dispose();
            dbc.removeBinding(viewerMSB);
            viewerMSB = null;
        }
    }

    /**
     * Filters out elements that are not visible. Monitors element visibility and updates the tree ridget.
     */
    private void monitorVisibility(final TreeViewer viewer, final TreeStructureAdvisor structureAdvisor,
            final IObservableMap visibilityAttr) {
        if (visibilityAttr != null) {
            viewer.addFilter(new ViewerFilter() {
                @Override
                public boolean select(final Viewer viewer, final Object parentElement, final Object element) {
                    final Object visible = visibilityAttr.get(element);
                    return Boolean.FALSE.equals(visible) ? false : true;
                }
            });
            final IMapChangeListener mapChangeListener = new IMapChangeListener() {
                public void handleMapChange(final MapChangeEvent event) {
                    final Set<?> affectedElements = event.diff.getChangedKeys();
                    for (final Object element : affectedElements) {
                        final Object parent = structureAdvisor.getParent(element);
                        if (parent == null || treeRoots.length == 0 || (parent == treeRoots[0] && !showRoots)) {
                            viewer.refresh();
                        } else {
                            viewer.refresh(parent);
                        }
                    }
                }
            };
            visibilityAttr.addMapChangeListener(mapChangeListener);
        }
    }

    /**
     * Prevent disabled items from being selected. This listener is executed before the SelectionTypeEnforcer.
     */
    private void preventDisabledItemSelection(final IObservableMap enablementAttr) {
        if (enablementAttr != null) {
            viewer.addSelectionChangedListener(new ISelectionChangedListener() {
                /* Holds the last selection. */
                private List<Object> lastSel;

                public void selectionChanged(final SelectionChangedEvent event) {
                    final IStructuredSelection selection = (IStructuredSelection) event.getSelection();
                    final List<Object> newSel = new ArrayList<Object>(selection.toList());
                    boolean changed = false;
                    for (final Object element : selection.toArray()) {
                        final Object isEnabled = enablementAttr.get(element);
                        if (Boolean.FALSE.equals(isEnabled)) {
                            newSel.remove(element);
                            changed = true;
                        }
                    }
                    if (changed) {
                        /*
                         * If the current selection is empty after rejecting disabled elements, restore the last selection.
                         */
                        if (newSel.isEmpty() && lastSel != null) {
                            viewer.setSelection(new StructuredSelection(lastSel));
                            setSelection(lastSel);
                        } else {
                            viewer.setSelection(new StructuredSelection(newSel));
                            setSelection(newSel);
                            lastSel = newSel;
                        }
                    } else {
                        lastSel = newSel;
                    }
                }
            });
        }
    }

    /**
     * Expand tree paths to candidates before selecting them. This ensures the tree items to the candidates are created and the candidates become
     * "known elements" (if they exist).
     */
    private void reveal(final Object[] candidates) {
        if (viewer != null) {
            final Control control = viewer.getControl();
            control.setRedraw(false);
            try {
                for (final Object candidate : candidates) {
                    viewer.expandToLevel(candidate, 0);
                }
            } finally {
                control.setRedraw(true);
            }
        }
    }

    /**
     * Take a snapshot of the selection in the tree widget.
     */
    private synchronized void saveSelection() {
        if (viewer != null && isOutputOnly()) {
            // only save selection when in 'output only' mode
            savedSelection = viewer.getTree().getSelection();
        } else {
            savedSelection = new TreeItem[0];
        }
    }

    /**
     * Resets the selection in the tree widget to the last saved selection.
     */
    private synchronized void restoreSelection() {
        if (viewer != null) {
            final Tree control = viewer.getTree();
            control.deselectAll();
            for (final TreeItem item : savedSelection) {
                // use select to avoid scrolling the tree
                control.select(item);
            }
        }
    }

    /**
     * Updates the expand / collapse state of the viewers model, based on a FIFO queue of {@link ExpansionCommand}s.
     */
    private void updateExpansionState() {
        if (viewer != null) {
            viewer.getControl().setRedraw(false);
            try {
                while (!expansionStack.isEmpty()) {
                    final ExpansionCommand cmd = expansionStack.remove();
                    final ExpansionState state = cmd.state;
                    if (state == ExpansionState.FULLY_COLLAPSE) {
                        final Object[] expanded = viewer.getExpandedElements();
                        viewer.collapseAll();
                        for (final Object wasExpanded : expanded) {
                            viewer.update(wasExpanded, null); // update icon
                        }
                    } else if (state == ExpansionState.FULLY_EXPAND) {
                        viewer.expandAll();
                        viewer.refresh(); // update all icons
                    } else if (state == ExpansionState.COLLAPSE) {
                        viewer.collapseToLevel(cmd.element, 1);
                        viewer.update(cmd.element, null); // update icon
                    } else if (state == ExpansionState.EXPAND) {
                        viewer.expandToLevel(cmd.element, 1);
                        viewer.update(cmd.element, null); // update icon
                    } else if (state == ExpansionState.RESTORE) {
                        final Object[] elements = (Object[]) cmd.element;
                        viewer.setExpandedElements(elements);
                    } else {
                        final String errorMsg = "unknown expansion state: " + state; //$NON-NLS-1$
                        throw new IllegalStateException(errorMsg);
                    }
                }

            } finally {
                viewer.getControl().setRedraw(true);
            }
        }
    }

    @Override
    protected StructuredViewerFilterHolder getFilterHolder() {
        if (filterHolder == null) {
            filterHolder = new StructuredViewerFilterHolder();
        }
        return filterHolder;
    }

    // helping classes
    // ////////////////

    /**
     * A {@link TreeViewer} that honors the current binding state of the Ridget
     */
    private final class SharedControlTreeViewer extends TreeViewer {

        private SharedControlTreeViewer(final Tree tree) {
            super(tree);
        }

        @Override
        protected void handleTreeExpand(final org.eclipse.swt.events.TreeEvent event) {
            if (getUIControl() != null) {
                if (!event.item.isDisposed()) {
                    super.handleTreeExpand(event);
                }
            }
        }

        @Override
        protected void handleTreeCollapse(final org.eclipse.swt.events.TreeEvent event) {
            if (getUIControl() != null) {
                super.handleTreeCollapse(event);
            }
        }
    }

    /**
     * Enumeration with the expansion states of this ridget.
     */
    private enum ExpansionState {
        FULLY_COLLAPSE, FULLY_EXPAND, COLLAPSE, EXPAND, RESTORE
    }

    /**
     * An operation that modifies the expansion state of the tree ridget.
     */
    private static final class ExpansionCommand {
        /** An expansion modification */
        private final ExpansionState state;
        /** The element to expand / collapse (only for COLLAPSE, EXPAND ops) */
        private final Object element;

        /**
         * Creates a new ExpansionCommand instance.
         * 
         * @param state
         *            an expansion modification
         * @param element
         *            the element to expand / collapse (null for FULLY_EXPAND / FULLY_COLLAPSE)
         */
        ExpansionCommand(final ExpansionState state, final Object element) {
            this.state = state;
            this.element = element;
        }
    }

    /**
     * Disallows multiple selection is the selection type of the ridget is {@link ISelectableRidget.SelectionType#SINGLE}.
     */
    private final class SelectionTypeEnforcer extends SelectionAdapter {
        @Override
        public void widgetSelected(final SelectionEvent e) {
            final Tree control = (Tree) e.widget;
            if (isOutputOnly()) {
                // ignore this event
                e.doit = false;
                restoreSelection();
            } else if (SelectionType.SINGLE.equals(getSelectionType())) {
                if (control.getSelectionCount() > 1) {
                    // ignore this event
                    e.doit = false;
                    // set selection one item
                    final TreeItem firstItem = control.getSelection()[0];
                    control.setSelection(firstItem);
                    // fire event
                    final Event event = new Event();
                    event.type = SWT.Selection;
                    event.doit = true;
                    control.notifyListeners(SWT.Selection, event);
                }
            }
        }
    }

    /**
     * Notifies doubleClickListeners when the bound widget is double clicked.
     */
    private final class DoubleClickForwarder extends MouseAdapter {
        @Override
        public void mouseDoubleClick(final MouseEvent e) {
            if (doubleClickListeners != null) {
                for (final IActionListener listener : doubleClickListeners.getListeners()) {
                    listener.callback();
                }
            }
        }
    }

    /**
     * This class is used as the tree viewer's input when showRoots is false.
     * <p>
     * It uses reflection to obtain the current list of children from the real root of the model, while keeping the input element (i.e. this instance) int the
     * tree all the time. This workaround allows us to update the level-0 of the tree without having to call setInput(...) on the tree viewer:
     * 
     * <pre>
     * FakeRoot fakeRoot;
     * viewer.setInput(...);
     * // ... later ...
     * fakeRoot.refresh();
     * viewer.refresh(fakeRoot);
     * </pre>
     * 
     * It uses reflection to obtain a n update list of children from the real root of the model.
     * 
     * @see TreeRidget#bindToModel(Object[], Class, String, String, String)
     * @see TreeContentProvider
     */
    static final class FakeRoot extends ArrayList<Object> {
        private static final long serialVersionUID = 1L;
        private final Object root0;
        private String accessor;

        FakeRoot(final Object root0, final String childrenAccessor) {
            Assert.isNotNull(childrenAccessor);
            clear();
            this.root0 = root0;
            if (root0 != null) {
                this.accessor = "get" + StringUtils.capitalize(childrenAccessor); //$NON-NLS-1$
                addAll(ReflectionUtils.<List<Object>>invoke(root0, accessor));
            }
        }

        Object getRoot() {
            return root0;
        }
    }

    /**
     * Advisor class for the Eclipse 3.4 tree databinding framework. See {link TreeStructureAdvisor}.
     * <p>
     * This advisor uses the supplied property name and elementClass to invoke an appropriate accessor (get/isXXX method) on a element in the tree.
     * <p>
     * This functionality is used by the databinding framework to perform expand operations.
     * 
     * @see TreeStructureAdvisor
     */
    private static final class GenericTreeStructureAdvisor extends TreeStructureAdvisor {

        private static final Object[] EMPTY_ARRAY = new Object[0];

        private final Class<?> beanClass;
        private PropertyDescriptor descriptor;

        GenericTreeStructureAdvisor(final String propertyName, final Class<?> elementClass) {
            Assert.isNotNull(propertyName);
            final String errorMsg = "propertyName cannot be empty"; //$NON-NLS-1$
            Assert.isLegal(propertyName.trim().length() > 0, errorMsg);
            Assert.isNotNull(elementClass);

            final String readMethodName = "get" + StringUtils.capitalize(propertyName); //$NON-NLS-1$
            try {
                descriptor = new PropertyDescriptor(propertyName, elementClass, readMethodName, null);
            } catch (final IntrospectionException exc) {
                log("Could not introspect bean.", exc); //$NON-NLS-1$
                descriptor = null;
            }
            this.beanClass = elementClass;
        }

        @Override
        public Object getParent(final Object element) {
            Object result = null;
            if (element != null && beanClass.isAssignableFrom(element.getClass()) && descriptor != null) {
                final Method readMethod = descriptor.getReadMethod();
                if (!readMethod.isAccessible()) {
                    readMethod.setAccessible(true);
                }
                try {
                    result = readMethod.invoke(element, EMPTY_ARRAY);
                } catch (final InvocationTargetException exc) {
                    log("Error invoking.", exc); //$NON-NLS-1$
                } catch (final IllegalAccessException exc) {
                    log("Error invoking.", exc); //$NON-NLS-1$
                }
            }
            return result;
        }

        private void log(final String message, final Exception exc) {
            final Logger logger = Log4r.getLogger(Activator.getDefault(), TreeRidget.class);
            logger.log(LogService.LOG_ERROR, message, exc);
        }
    }

    /**
     * This change listener reacts to additions / removals of objects from the tree and is responsible for updating the image of the <b>parent</b> element.
     * Specifically:
     * <ul>
     * <li>if B gets added to A we have to refresh the icon of A, if A did not have any children beforehand</li>
     * <li>if B gets removed to A we have to refresh the icon of A, if B was the last child underneath A</li>
     * <ul>
     */
    private static final class TreeContentChangeListener implements ISetChangeListener {

        private final TreeViewer viewer;
        private final TreeStructureAdvisor structureAdvisor;

        private TreeContentChangeListener(final TreeViewer viewer, final TreeStructureAdvisor structureAdvisor) {
            Assert.isNotNull(structureAdvisor);
            this.structureAdvisor = structureAdvisor;
            this.viewer = viewer;
            Assert.isNotNull(viewer.getContentProvider());
        }

        /**
         * Updates the icons of the parent elements on addition / removal
         */
        public void handleSetChange(final SetChangeEvent event) {
            if (viewer.getLabelProvider(0) == null) {
                return;
            }
            final Set<Object> parents = new HashSet<Object>();
            for (final Object element : event.diff.getAdditions()) {
                final Object parent = structureAdvisor.getParent(element);
                if (parent != null) {
                    parents.add(parent);
                }
            }
            for (final Object element : event.diff.getRemovals()) {
                final Object parent = structureAdvisor.getParent(element);
                if (parent != null) {
                    parents.add(parent);
                }
            }
            for (final Object parent : parents) {
                if (!viewer.isBusy()) {
                    viewer.update(parent, null);
                }
            }
        }
    }
}