org.eclipse.debug.internal.ui.viewers.AsynchronousViewer.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.debug.internal.ui.viewers.AsynchronousViewer.java

Source

/*******************************************************************************
 * Copyright (c) 2005, 2011 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.debug.internal.ui.viewers;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.internal.ui.DebugUIPlugin;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelChangedListener;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxy;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelSelectionPolicy;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelSelectionPolicyFactory;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IStatusMonitor;
import org.eclipse.debug.internal.ui.viewers.model.provisional.PresentationContext;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.progress.WorkbenchJob;

/**
 * A viewer that retrieves labels and content asynchronously via adapters and supports
 * duplicate elements in the viewer. Retrieving content and labels asynchronously allows
 * for arbitrary latency without blocking the UI thread.
 * <p>
 * This viewer uses adapters to retrieve labels and content rather than
 * a label provider and content provider. As such, the label provider for this viewer
 * is <code>null</code> by default. The content provider returned by this viewer is
 * non-<code>null</code> to conform to the viewer specification, but performs no 
 * useful function.
 * </p>
 * <p>
 * The selection in this viewer is also set asynchronously. When the selection is set,
 * the viewer attempts to perform the selection. If the elements in the specified selection
 * are not yet in the viewer, the portion of the selection that could not be honored
 * becomes a pending selection. As more elements are added to viewer, the pending selection
 * is attempted to be set.  
 * </p>
 * @since 3.2
 */
public abstract class AsynchronousViewer extends StructuredViewer implements Listener {

    /**
     * Model of elements for this viewer
     */
    private AsynchronousModel fModel;

    /**
     * Cache of images used for elements in this tree viewer. Label updates
     * use the method <code>getImage(...)</code> to cache images for
     * image descriptors. The images are disposed when this viewer is disposed.
     */
    private Map fImageCache = new HashMap();

    /**
     * Cache of the fonts used for elements in this tree viewer. Label updates
     * use the method <code>getFont(...)</code> to cache fonts for
     * FontData objects. The fonts are disposed with the viewer.
     */
    private Map fFontCache = new HashMap();

    /**
     * Cache of the colors used for elements in this tree viewer. Label updates
     * use the method <code>getColor(...)</code> to cache colors for
     * RGB values. The colors are disposed with the viewer.
     */
    private Map fColorCache = new HashMap();

    /**
     * The context in which this viewer is being used - i.e. what part it is contained
     * in any any preference settings associated with it.
     */
    private IPresentationContext fContext;

    private ISelection fPendingSelection;

    private ISelection fCurrentSelection;

    /**
     * Array used to store indices of the path to an item in the viewer being mapped
     * by a 'set data' callback. Indices are bottom up. For example when 'set data' for 
     * the 3rd child of the 4th child of the 2nd root element were being asked for,
     * the first 3 indices would look like: [3, 4, 2, ....]. We re-use an array to avoid
     * creating a new one all the time. The array grows as needed to accommodate deep
     * elements.
     */
    private int[] fSetDataIndicies = new int[5];

    /**
     * The update policy for this viewer.
     */
    private AbstractUpdatePolicy fUpdatePolicy;

    protected static final String OLD_LABEL = "old_label"; //$NON-NLS-1$
    protected static final String OLD_IMAGE = "old_image"; //$NON-NLS-1$

    // debug flags
    public static boolean DEBUG_VIEWER = false;

    static {
        DEBUG_VIEWER = DebugUIPlugin.DEBUG && "true".equals( //$NON-NLS-1$
                Platform.getDebugOption("org.eclipse.debug.ui/debug/viewers/viewer")); //$NON-NLS-1$
    }

    /**
     * Creates a new viewer 
     */
    protected AsynchronousViewer() {
        setContentProvider(new NullContentProvider());
        setUseHashlookup(true);
    }

    /**
     * Hash lookup is required, don't let subclasses change behavior.
     * @param enable if hash lookup should be used in the viewer
     */
    public final void setUseHashlookup(boolean enable) {
        Assert.isTrue(enable);
        super.setUseHashlookup(enable);
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#hookControl(org.eclipse.swt.widgets.Control)
     */
    protected void hookControl(Control control) {
        super.hookControl(control);
        control.addListener(SWT.SetData, this);
    }

    /**
     * Clients must call this methods when this viewer is no longer needed
     * so it can perform cleanup.
     */
    public synchronized void dispose() {
        Iterator images = fImageCache.values().iterator();
        while (images.hasNext()) {
            Image image = (Image) images.next();
            image.dispose();
        }
        fImageCache.clear();

        Iterator fonts = fFontCache.values().iterator();
        while (fonts.hasNext()) {
            Font font = (Font) fonts.next();
            font.dispose();
        }
        fFontCache.clear();

        Iterator colors = fColorCache.values().iterator();
        while (colors.hasNext()) {
            Color color = (Color) colors.next();
            color.dispose();
        }
        fColorCache.clear();

        if (fModel != null) {
            fModel.dispose();
        }
        if (fUpdatePolicy != null) {
            fUpdatePolicy.dispose();
        }
        if (fContext != null) {
            ((PresentationContext) fContext).dispose();
        }
    }

    /**
     * Updates all occurrences of the given element in this viewer.
     * 
     * @param element element to update
     */
    public void update(Object element) {
        ModelNode[] nodes = getModel().getNodes(element);
        if (nodes != null) {
            for (int i = 0; i < nodes.length; i++) {
                updateLabel(nodes[i]);
            }
        }
    }

    /**
     * Updates the label for a specific element (node) in the model.
     * 
     * @param node node to update
     */
    protected void updateLabel(ModelNode node) {
        // the input is not displayed
        if (!node.getElement().equals(getInput())) {
            getModel().updateLabel(node);
        }
    }

    /**
     * Returns the presentation context to be used in update requests.
     * Clients may override this method if required to provide special
     * implementations of contexts.
     * 
     * @return presentation context
     */
    public IPresentationContext getPresentationContext() {
        return fContext;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#unmapAllElements()
     */
    protected synchronized void unmapAllElements() {
        super.unmapAllElements();
        AsynchronousModel model = getModel();
        if (model != null) {
            model.dispose();
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.Viewer#inputChanged(java.lang.Object, java.lang.Object)
     */
    protected synchronized void inputChanged(Object input, Object oldInput) {
        fPendingSelection = null;
        if (fCurrentSelection != null) {
            updateSelection(new StructuredSelection());
            fCurrentSelection = null;
        }
        if (fUpdatePolicy == null) {
            fUpdatePolicy = createUpdatePolicy();
            fUpdatePolicy.init(this);
        }
        if (fModel != null) {
            fModel.dispose();
        }
        fModel = createModel();
        fModel.init(input);
        if (input != null) {
            mapElement(fModel.getRootNode(), getControl());
            getControl().setData(fModel.getRootNode().getElement());
        } else {
            unmapAllElements();
            getControl().setData(null);
        }
        refresh();
    }

    /**
     * Creates a new empty model for this viewer that
     * is *not* initialized.
     * 
     * @return a new model
     */
    protected abstract AsynchronousModel createModel();

    /**
     * Creates and returns this viewers update policy.
     * @return update policy
     */
    public abstract AbstractUpdatePolicy createUpdatePolicy();

    Image[] getImages(ImageDescriptor[] descriptors) {
        if (descriptors == null || descriptors.length == 0) {
            String[] columns = getPresentationContext().getColumns();
            if (columns == null) {
                return new Image[1];
            } else {
                return new Image[columns.length];
            }
        }
        Image[] images = new Image[descriptors.length];
        for (int i = 0; i < images.length; i++) {
            images[i] = getImage(descriptors[i]);
        }
        return images;
    }

    /**
     * Returns an image for the given image descriptor or <code>null</code>. Adds the image
     * to a cache of images if it does not already exist. The cache is cleared when this viewer
     * is disposed. 
     * 
     * @param descriptor image descriptor or <code>null</code>
     * @return image or <code>null</code>
     */
    protected Image getImage(ImageDescriptor descriptor) {
        if (descriptor == null) {
            return null;
        }
        Image image = (Image) fImageCache.get(descriptor);
        if (image == null) {
            image = new Image(getControl().getDisplay(), descriptor.getImageData());
            fImageCache.put(descriptor, image);
        }
        return image;
    }

    protected Font[] getFonts(FontData[] fontDatas) {
        if (fontDatas == null || fontDatas.length == 0) {
            String[] columns = getPresentationContext().getColumns();
            if (columns == null) {
                return new Font[1];
            } else {
                return new Font[columns.length];
            }
        }

        Font[] fonts = new Font[fontDatas.length];
        for (int i = 0; i < fonts.length; i++) {
            fonts[i] = getFont(fontDatas[i]);
        }
        return fonts;
    }

    /**
     * Returns a font for the given font data or <code>null</code>. Adds the font to this viewer's font 
     * cache which is disposed when this viewer is disposed.
     * 
     * @param fontData font data or <code>null</code>
     * @return font font or <code>null</code>
     */
    protected Font getFont(FontData fontData) {
        if (fontData == null) {
            return null;
        }
        Font font = (Font) fFontCache.get(fontData);
        if (font == null) {
            font = new Font(getControl().getDisplay(), fontData);
            fFontCache.put(fontData, font);
        }
        return font;
    }

    protected Color[] getColors(RGB[] rgb) {
        if (rgb == null || rgb.length == 0) {
            String[] columns = getPresentationContext().getColumns();
            if (columns == null) {
                return new Color[1];
            } else {
                return new Color[columns.length];
            }
        }
        Color[] colors = new Color[rgb.length];
        for (int i = 0; i < colors.length; i++) {
            colors[i] = getColor(rgb[i]);
        }
        return colors;
    }

    /**
     * Returns a color for the given RGB or <code>null</code>. Adds the color to this viewer's color 
     * cache which is disposed when this viewer is disposed.
     * 
     * @param rgb RGB or <code>null</code>
     * @return color or <code>null</code>
     */
    protected Color getColor(RGB rgb) {
        if (rgb == null) {
            return null;
        }
        Color color = (Color) fColorCache.get(rgb);
        if (color == null) {
            color = new Color(getControl().getDisplay(), rgb);
            fColorCache.put(rgb, color);
        }
        return color;
    }

    /**
     * Sets the context for this viewer. 
     * 
     * @param context the presentation context
     */
    public void setContext(IPresentationContext context) {
        fContext = context;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#doFindItem(java.lang.Object)
     */
    protected Widget doFindItem(Object element) {
        // this viewer maps model nodes to widgets, so the element is a ModelNode
        AsynchronousModel model = getModel();
        if (model != null) {
            if (element.equals(model.getRootNode())) {
                return doFindInputItem(element);
            }
            Widget[] widgets = findItems(element);
            if (widgets.length > 0) {
                return widgets[0];
            }
        }
        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.jface.viewers.StructuredViewer#doFindInputItem(java.lang.Object)
     */
    protected Widget doFindInputItem(Object element) {
        if (element instanceof ModelNode) {
            ModelNode node = (ModelNode) element;
            if (node.getElement().equals(getInput())) {
                return getControl();
            }
        }
        return null;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#doUpdateItem(org.eclipse.swt.widgets.Widget, java.lang.Object, boolean)
     */
    protected void doUpdateItem(Widget item, Object element, boolean fullMap) {
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#internalRefresh(java.lang.Object)
     */
    protected void internalRefresh(Object element) {
        // get the nodes in the model
        AsynchronousModel model = getModel();
        if (model != null) {
            ModelNode[] nodes = model.getNodes(element);
            if (nodes != null) {
                for (int i = 0; i < nodes.length; i++) {
                    ModelNode node = nodes[i];
                    // get the widget for the node
                    Widget item = findItem(node);
                    if (item != null) {
                        internalRefresh(node);
                    }
                }
            }
        }
    }

    /**
     * Refreshes a specific occurrence of an element (a node).
     * 
     * @param node node to update
     * 
     * Subclasses should override and call super
     */
    protected void internalRefresh(ModelNode node) {
        updateLabel(node);
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.Viewer#setSelection(org.eclipse.jface.viewers.ISelection, boolean)
     */
    public synchronized void setSelection(ISelection selection, boolean reveal) {
        setSelection(selection, reveal, false);
    }

    /**
     * Sets the selection in this viewer.
     * 
     * @param selection new selection
     * @param reveal whether to reveal the selection
     * @param force whether to force the selection change without consulting the model
     *  selection policy
     */
    public synchronized void setSelection(ISelection selection, final boolean reveal, boolean force) {
        Control control = getControl();
        if (control == null || control.isDisposed()) {
            return;
        }
        if (!acceptsSelection(selection)) {
            selection = getEmptySelection();
        }
        if (!force && !overrideSelection(fCurrentSelection, selection)) {
            return;
        }

        fPendingSelection = selection;

        if (getControl().getDisplay().getThread() == Thread.currentThread()) {
            attemptSelection(reveal);
        } else {
            WorkbenchJob job = new WorkbenchJob("attemptSelection") { //$NON-NLS-1$
                public IStatus runInUIThread(IProgressMonitor monitor) {
                    attemptSelection(reveal);
                    return Status.OK_STATUS;
                }

            };
            job.setSystem(true);
            job.schedule();
        }
    }

    /**
     * Returns whether the candidate selection should override the current
     * selection.
     * 
     * @param current the current selection
     * @param candidate the new selection
     * @return if the selection should be overridden
     */
    protected boolean overrideSelection(ISelection current, ISelection candidate) {
        IModelSelectionPolicy selectionPolicy = getSelectionPolicy(current);
        if (selectionPolicy == null) {
            return true;
        }
        if (selectionPolicy.contains(candidate, getPresentationContext())) {
            return selectionPolicy.overrides(current, candidate, getPresentationContext());
        }
        return !selectionPolicy.isSticky(current, getPresentationContext());
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#getSelection()
     */
    public ISelection getSelection() {
        Control control = getControl();
        if (control == null || control.isDisposed() || fCurrentSelection == null) {
            return StructuredSelection.EMPTY;
        }
        return fCurrentSelection;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#handleSelect(org.eclipse.swt.events.SelectionEvent)
     */
    protected void handleSelect(SelectionEvent event) {
        // handle case where an earlier selection listener disposed the control.
        Control control = getControl();
        if (control != null && !control.isDisposed()) {
            updateSelection(newSelectionFromWidget());
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#handlePostSelect(org.eclipse.swt.events.SelectionEvent)
     */
    protected void handlePostSelect(SelectionEvent e) {
        SelectionChangedEvent event = new SelectionChangedEvent(this, newSelectionFromWidget());
        firePostSelectionChanged(event);
    }

    /**
     * Creates and returns a new selection from this viewer, based on the selected
     * elements in the widget.
     * 
     * @return a new selection
     */
    protected abstract ISelection newSelectionFromWidget();

    /**
     * Returns the selection policy associated with the given selection
     * or <code>null</code> if none.
     * 
     * @param selection or <code>null</code>
     * @return selection policy or <code>null</code>
     */
    protected IModelSelectionPolicy getSelectionPolicy(ISelection selection) {
        if (selection instanceof IStructuredSelection) {
            IStructuredSelection ss = (IStructuredSelection) selection;
            Object element = ss.getFirstElement();
            if (element instanceof IAdaptable) {
                IAdaptable adaptable = (IAdaptable) element;
                IModelSelectionPolicyFactory factory = (IModelSelectionPolicyFactory) adaptable
                        .getAdapter(IModelSelectionPolicyFactory.class);
                if (factory != null) {
                    return factory.createModelSelectionPolicyAdapter(adaptable, getPresentationContext());
                }
            }
        }
        return null;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#setSelectionToWidget(org.eclipse.jface.viewers.ISelection, boolean)
     */
    final protected void setSelectionToWidget(ISelection selection, final boolean reveal) {
        // NOT USED
        throw new IllegalArgumentException("This method should not be called"); //$NON-NLS-1$
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#setSelectionToWidget(java.util.List, boolean)
     */
    final protected void setSelectionToWidget(List l, boolean reveal) {
        // NOT USED
        throw new IllegalArgumentException("This method should not be called"); //$NON-NLS-1$
    }

    /**
     * Attempts to update any pending selection.
     * 
     * @param reveal whether to reveal the selection
     */
    protected void attemptSelection(boolean reveal) {
        ISelection currentSelection = null;
        synchronized (this) {
            if (fPendingSelection != null) {
                ISelection remaining = doAttemptSelectionToWidget(fPendingSelection, reveal);
                if (remaining.isEmpty()) {
                    remaining = null;
                }
                if (!fPendingSelection.equals(remaining)) {
                    fPendingSelection = remaining;
                    currentSelection = newSelectionFromWidget();
                    if (isSuppressEqualSelections() && currentSelection.equals(fCurrentSelection)) {
                        return;
                    }
                }
            }
        }
        if (currentSelection != null) {
            updateSelection(currentSelection);
            firePostSelectionChanged(new SelectionChangedEvent(this, currentSelection));
        }
    }

    /**
     * Controls whether selection change notification is sent even when
     * successive selections are equal.
     * 
     * TODO: what we really want is to fire selection change on ACTIVATE model
     * change, even when selection is the same.
     * 
     * @return whether to suppress change notification for equal successive
     *         selections
     */
    protected boolean isSuppressEqualSelections() {
        return true;
    }

    /**
     * Attempts to selection the specified selection and returns a selection
     * representing the portion of the selection that could not be honored
     * and still needs to be selected.
     * 
     * @param selection selection to attempt
     * @param reveal whether to reveal the selection
     * @return remaining selection
     */
    protected abstract ISelection doAttemptSelectionToWidget(ISelection selection, boolean reveal);

    /**
     * Returns whether this viewer supports the given selection.
     * 
     * @param selection a selection
     * @return whether this viewer supports the given selection
     */
    protected abstract boolean acceptsSelection(ISelection selection);

    /**
     * Returns an empty selection supported by this viewer.
     * 
     * @return an empty selection supported by this viewer
     */
    protected abstract ISelection getEmptySelection();

    /**
     * A content provider that does nothing.
     */
    private class NullContentProvider implements IStructuredContentProvider {
        public void dispose() {
        }

        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        }

        public Object[] getElements(Object inputElement) {
            return null;
        }
    }

    /**
     * Notification that a presentation update has failed.
     * Subclasses may override as required. The default implementation
     * does nothing.
     * 
     * @param monitor monitor for the presentation request that failed
     * @param status status of update
     */
    protected void handlePresentationFailure(IStatusMonitor monitor, IStatus status) {
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#preservingSelection(java.lang.Runnable)
     */
    protected synchronized void preservingSelection(Runnable updateCode) {
        if (fPendingSelection == null || fPendingSelection.isEmpty()) {
            ISelection oldSelection = null;
            try {
                // preserve selection
                oldSelection = fCurrentSelection;
                // perform the update
                updateCode.run();
            } finally {
                // restore selection
                if (oldSelection == null) {
                    oldSelection = new StructuredSelection();
                }
                if (getControl().getDisplay().getThread() == Thread.currentThread()) {
                    if (!oldSelection.equals(newSelectionFromWidget())) {
                        restoreSelection(oldSelection);
                    }
                } else {
                    WorkbenchJob job = new WorkbenchJob("attemptSelection") { //$NON-NLS-1$
                        public IStatus runInUIThread(IProgressMonitor monitor) {
                            synchronized (AsynchronousViewer.this) {
                                if (!getControl().isDisposed()) {
                                    if (fPendingSelection == null || fPendingSelection.isEmpty()) {
                                        ISelection tempSelection = fCurrentSelection;
                                        if (tempSelection == null) {
                                            tempSelection = new StructuredSelection();
                                        }
                                        if (!tempSelection.equals(newSelectionFromWidget())) {
                                            restoreSelection(tempSelection);
                                        }
                                    }
                                }
                            }
                            return Status.OK_STATUS;
                        }

                    };
                    job.setSystem(true);
                    job.schedule();
                }
            }
        } else {
            updateCode.run();
        }
    }

    protected synchronized void restoreSelection(ISelection oldSelection) {
        ISelection remaining = doAttemptSelectionToWidget(oldSelection, false);
        // send out notification if old and new differ
        fCurrentSelection = newSelectionFromWidget();
        if (!selectionExists(fCurrentSelection)) {
            if (selectionExists(oldSelection)) {
                // old selection exists in the model, but not widget
                fCurrentSelection = oldSelection;
            } else {
                fCurrentSelection = getEmptySelection();
            }
        }
        if (!fCurrentSelection.equals(oldSelection)) {
            handleInvalidSelection(oldSelection, fCurrentSelection);
            // if the remaining selection still exists in the model, make it pending
            if (selectionExists(remaining)) {
                setSelection(remaining);
            }
        }
    }

    /**
     * Returns whether the selection exists in the model
     * @param selection the selection context
     * @return <code>true</code> if the selecton exists in the model <code>false</code> otherwise
     */
    protected boolean selectionExists(ISelection selection) {
        if (selection.isEmpty()) {
            return false;
        }
        if (selection instanceof IStructuredSelection) {
            IStructuredSelection ss = (IStructuredSelection) selection;
            Iterator iterator = ss.iterator();
            while (iterator.hasNext()) {
                Object element = iterator.next();
                if (getModel().getNodes(element) == null) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Sets the color attributes of the given widget.
     * 
     * @param widget the widget to update
     * @param foreground foreground color of the widget or <code>null</code> if default
     * @param background background color of the widget or <code>null</code> if default
     */
    protected abstract void setColors(Widget widget, RGB foreground[], RGB background[]);

    /**
     * Sets the label attributes of the given widget.
     * 
     * @param widget the widget to update
     * @param text label text
     * @param image label image or <code>null</code>
     */
    protected abstract void setLabels(Widget widget, String[] text, ImageDescriptor[] image);

    /**
     * Sets the font attributes of the given widget.
     * 
     * @param widget widget to update
     * @param font font of the widget or <code>null</code> if default.
     */
    protected abstract void setFonts(Widget widget, FontData[] font);

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.StructuredViewer#updateSelection(org.eclipse.jface.viewers.ISelection)
     */
    protected synchronized void updateSelection(ISelection selection) {
        fCurrentSelection = selection;
        super.updateSelection(selection);
    }

    /**
     * Notification the given model proxy has been added to this viewer's model.
     * 
     * @param proxy the model proxy that has been added
     */
    protected void modelProxyAdded(IModelProxy proxy) {
        if (fUpdatePolicy instanceof IModelChangedListener) {
            proxy.addModelChangedListener((IModelChangedListener) fUpdatePolicy);
        }
    }

    /**
     * Notification the given model proxy has been removed from this viewer's model.
     * 
     * @param proxy the model proxy that has been removed
     */
    protected void modelProxyRemoved(IModelProxy proxy) {
        if (fUpdatePolicy instanceof IModelChangedListener) {
            proxy.removeModelChangedListener((IModelChangedListener) fUpdatePolicy);
        }
    }

    /**
     * Returns this viewer's model
     * 
     * @return model
     */
    protected AsynchronousModel getModel() {
        return fModel;
    }

    /**
     * A node in the model has been updated
     * 
     * @param node the model node that has been changed
     */
    protected void nodeChanged(ModelNode node) {
        Widget widget = findItem(node);
        if (widget != null) {
            clear(widget);
            attemptPendingUpdates();
        }
    }

    /**
     * @return if there are any more pending updates in the viewer
     */
    public synchronized boolean hasPendingUpdates() {
        return getModel().hasPendingUpdates();
    }

    /**
     * Notification from the model that the update for the given request
     * has completed.
     * 
     * @param monitor the monitor
     */
    protected void updateComplete(IStatusMonitor monitor) {
    }

    /**
     * Clears the given widget
     * 
     * @param item the widget
     */
    protected abstract void clear(Widget item);

    /**
     * Clears the children of the widget.
     * 
     * @param item the widget to clear children from
     */
    protected abstract void clearChildren(Widget item);

    /**
     * Clears the child at the given index.
     * 
     * @param parent the parent widget
     * @param childIndex the index of the child widget to clear
     */
    protected abstract void clearChild(Widget parent, int childIndex);

    /**
     * Returns the child widget at the given index for the given parent or
     * <code>null</code>
     * 
     * @param parent the parent widget
     * @param index the index of the child in the parent widget 
     * @return the widget at the given index in the parent or <code>null</code>
     */
    protected abstract Widget getChildWidget(Widget parent, int index);

    /**
     * Sets the item count for a parent widget
     * 
     * @param parent the parent widget
     * @param itemCount the new item count to set
     */
    protected abstract void setItemCount(Widget parent, int itemCount);

    /**
     * Attempt pending updates. Subclasses may override but should call super.
     */
    protected void attemptPendingUpdates() {
        attemptSelection(false);
    }

    /**
     * Notification a node's children have changed.
     * Updates the child count for the parent's widget
     * and clears children to be updated.
     * 
     * @param parentNode the parent model node
     */
    protected void nodeChildrenChanged(ModelNode parentNode) {
        Widget widget = findItem(parentNode);
        if (widget != null && !widget.isDisposed()) {
            int childCount = parentNode.getChildCount();
            setItemCount(widget, childCount);
            clearChildren(widget);
            attemptPendingUpdates();
        }
    }

    /**
     * Notification children have been added to the end
     * of the given parent.
     * 
     * @param parentNode the parent model node
     */
    protected void nodeChildrenAdded(ModelNode parentNode) {
        Widget widget = findItem(parentNode);
        if (widget != null && !widget.isDisposed()) {
            int childCount = parentNode.getChildCount();
            setItemCount(widget, childCount);
            attemptPendingUpdates();
        }
    }

    /**
     * Notification children have been added to the end
     * of the given parent.
     * 
     * @param parentNode the parent model node
     * @param index the index of the child that was removed 
     */
    protected void nodeChildRemoved(ModelNode parentNode, int index) {
        Widget widget = findItem(parentNode);
        if (widget != null && !widget.isDisposed()) {
            Widget childWidget = getChildWidget(widget, index);
            int childCount = parentNode.getChildCount();
            // if the child widget exists, dispose it so item state remains, otherwise update child count
            if (childWidget == null) {
                setItemCount(widget, childCount);
            } else {
                childWidget.dispose();
            }
            for (int i = index; i < childCount; i++) {
                clearChild(widget, i);
            }
            attemptPendingUpdates();
        }
    }

    /**
     * Unmaps the node from its widget and all of its children nodes from
     * their widgets.
     * 
     * @param node the model node
     */
    protected void unmapNode(ModelNode node) {
        unmapElement(node);
        ModelNode[] childrenNodes = node.getChildrenNodes();
        if (childrenNodes != null) {
            for (int i = 0; i < childrenNodes.length; i++) {
                unmapNode(childrenNodes[i]);
            }
        }
    }

    /**
     * Returns the node corresponding to the given widget or <code>null</code>
     * @param widget widget for which a node is requested
     * @return node or <code>null</code>
     */
    protected ModelNode findNode(Widget widget) {
        ModelNode[] nodes = getModel().getNodes(widget.getData());
        if (nodes != null) {
            for (int i = 0; i < nodes.length; i++) {
                ModelNode node = nodes[i];
                Widget item = findItem(node);
                if (widget == item) {
                    return node;
                }
            }
        }
        return null;
    }

    /**
     * Returns the item for the node or <code>null</code>
     * @param node the model node
     * @return the widget or <code>null</code>
     */
    protected Widget findItem(ModelNode node) {
        return findItem((Object) node);
    }

    /*
     * (non-Javadoc)
     * 
     * A virtual item has been exposed in the control, map its data.
     * 
     * @see org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets.Event)
     */
    public void handleEvent(final Event event) {
        update((Item) event.item, event.index);
    }

    /**
     * Update the given item.
     * 
     * @param item item to update
     * @param index index of item in parent's children
     */
    protected void update(Item item, int index) {
        restoreLabels(item);
        int level = 0;

        Widget parentItem = getParentWidget(item);
        if (DEBUG_VIEWER) {
            DebugUIPlugin.debug("SET DATA [" + index + "]: " + parentItem); //$NON-NLS-1$//$NON-NLS-2$
        }
        ModelNode node = null;
        // first, see if the parent element is in the model
        // and look directly for the child
        if (parentItem != null) {
            ModelNode[] nodes = getModel().getNodes(parentItem.getData());
            if (nodes != null) {
                for (int i = 0; i < nodes.length; i++) {
                    ModelNode parentNode = nodes[i];
                    Widget parentWidget = findItem(parentNode);
                    if (parentWidget == parentItem) {
                        ModelNode[] childrenNodes = parentNode.getChildrenNodes();
                        if (childrenNodes != null && index < childrenNodes.length) {
                            node = childrenNodes[index];
                        }
                    }
                }
            }
        }

        // otherwise, build a path to the model node
        if (node == null) {
            setNodeIndex(index, level);
            while (parentItem instanceof Item) {
                level++;
                Widget parent = getParentWidget(parentItem);
                int pindex = indexOf(parent, parentItem);
                if (pindex < 0) {
                    return;
                }
                setNodeIndex(pindex, level);
                parentItem = parent;
            }

            node = getModel().getRootNode();
            if (node == null) {
                if (DEBUG_VIEWER) {
                    DebugUIPlugin.debug("\tFAILED - root model node is null"); //$NON-NLS-1$
                }
                return;
            }
            for (int i = level; i >= 0; i--) {
                ModelNode[] childrenNodes = node.getChildrenNodes();
                if (childrenNodes == null) {
                    if (DEBUG_VIEWER) {
                        DebugUIPlugin.debug("\tFAILED - no children nodes for " + node); //$NON-NLS-1$
                    }
                    return;
                }
                int pindex = getNodeIndex(i);
                if (pindex < childrenNodes.length) {
                    node = childrenNodes[pindex];
                } else {
                    if (DEBUG_VIEWER) {
                        DebugUIPlugin.debug("\tFAILED - no children nodes for " + node); //$NON-NLS-1$
                    }
                    return;
                }
            }
        }

        // map the node to the element and refresh it
        if (node != null) {
            mapElement(node, item);
            item.setData(node.getElement());
            if (DEBUG_VIEWER) {
                DebugUIPlugin.debug("\titem mapped: " + node); //$NON-NLS-1$
            }
            internalRefresh(node);
        } else {
            if (DEBUG_VIEWER) {
                DebugUIPlugin.debug("\tFAILED - unable to find corresponding node"); //$NON-NLS-1$
            }
        }
    }

    /**
     * Sets the index of a child node being mapped at the given expansion level
     * in the tree.
     * 
     * @param nodeIndex the index of the node
     * @param level the expansion level
     */
    private void setNodeIndex(int nodeIndex, int level) {
        if (level > (fSetDataIndicies.length - 1)) {
            // grow the array
            int[] next = new int[level + 5];
            System.arraycopy(fSetDataIndicies, 0, next, 0, fSetDataIndicies.length);
            fSetDataIndicies = next;
        }
        fSetDataIndicies[level] = nodeIndex;
    }

    /**
     * Returns the index of a child node being mapped at the given expansion level in
     * the tree.
     * 
     * @param level the expansion level
     * @return the child index
     */
    private int getNodeIndex(int level) {
        return fSetDataIndicies[level];
    }

    protected abstract int indexOf(Widget parent, Widget child);

    protected abstract void restoreLabels(Item item);

    /**
     * Returns the parent widget for the given widget or <code>null</code>
     * 
     * @param widget the widget to get the parent from
     * @return parent widget or <code>null</code>
     */
    protected abstract Widget getParentWidget(Widget widget);

    /**
     * Updates the children of the given node.
     * 
     * @param parent
     *            node of which to update children
     */
    protected void updateChildren(ModelNode parent) {
        getModel().updateChildren(parent);
    }
}