/******************************************************************************* * 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.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); } } } } }