Home > Eclipse development, Software Development > Writing an Eclipse Plug-in (Part 12): Common Navigator: Keeping the Tree Open When a New Resource is Added

Writing an Eclipse Plug-in (Part 12): Common Navigator: Keeping the Tree Open When a New Resource is Added


Welcome back, boys and girls. In this installment of Writing an Eclipse Plug-in we add the phenomenally simple behavior of keeping the expanded tree nodes open when we add new projects.

The Use Case:

  1. Create a custom project.
  2. Open one of the nodes
  3. Create another custom project
  4. Both projects should appear and the first projects node should still be opened

In our usual test-driven behavior, try the above and see the navigator fail the test. Let’s fix that.

What to do

  1. Open the customnavigator ContentProvider.
  2. Change resourceChanged with the following bold code:
  3. ContentProvider.java

        @Override
        public void resourceChanged(IResourceChangeEvent event) {
            TreeViewer viewer = (TreeViewer) _viewer;
    
            TreePath[] treePaths = viewer.getExpandedTreePaths();
            viewer.refresh();
            viewer.setExpandedTreePaths(treePaths);
        }
    
  4. Add this field definition to the top of the file:
  5.     private Map<String, Object> _wrapperCache = new HashMap<String, Object>();
    
  6. Make the following changes to createCustomProjectParents():
  7.     private Object[] createCustomProjectParents(IProject[] projects) {
            Object[] result = null;
    
            List<Object> list = new ArrayList<Object>();
            for (int i = 0; i < projects.length; i++) {
                Object customProjectParent = _wrapperCache.get(projects[i].getName());
                if (customProjectParent == null) {
                    customProjectParent = createCustomProjectParent(projects[i]);
                    _wrapperCache.put(projects[i].getName(), customProjectParent);
                }
    
                if (customProjectParent != null) {
                    list.add(customProjectParent);
                } // else ignore the project
            }
    
            result = new Object[list.size()];
            list.toArray(result);
    
            return result;
        }
    
  8. Start the runtime workbench, create a project, open one of the tree nodes, and create another project. The first project should look the same (the node you opened should still be opened)

Take a bow.

Why Did We Do That?

When you play with the custom navigator you can’t help but notice how many things the navigator does not do including maintain its look when a new project is added. Since my current goal in life is to scratch my CNF itch here is why the steps above work.

Change resourceChanged() with the following bold code:

ContentProvider.java

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        TreeViewer viewer = (TreeViewer) _viewer;

        TreePath[] treePaths = viewer.getExpandedTreePaths();
        viewer.refresh();
        viewer.setExpandedTreePaths(treePaths);
    }

In order to get the above behavior to work we could have also used TreeViewer.getExpandedElements()/setExpandedElements() instead of TreeViewer.getExpandedTreePaths()/setExpandedTreePaths(). For whatever reason, TreeViewer.getExpandedElements()/setExpandedElements() works just as well as TreeViewer.getExpandedTreePaths()/setExpandedTreePaths(). One day (I’ll care enough to) I’ll figure out why.

If you recall, in order to get the navigator to update itself we have to call viewer.refresh(). Well, in order to get the viewer to display whatever it is we opened we have to ask it what was opened before the refresh(); hence the call to viewer.getExpandedTreePaths(). After we refresh the viewer then we have to tell it which nodes to reopen as it closes all of them on a refresh; hence the call to viewer.setExpandedTreePaths(treePaths).

Pretty easy. Start the runtime workbench and create a project, open a node and create a new project. Hmm. Still doesn’t work.

The reason why the code above doesn’t work is because of the way the resource elements are wrapped; every time getChildren() is called we create new wrappers. New wrappers means new object references. New object references means new values returned from hashCode(). New hashCode() values means that equals() will behave as if the same project is really a new project. Nothing confuses the viewer more than giving it the impression that new nodes need to be displayed. To fix this we need to cache the wrappers around the projects and return them when we can and create new ones when needed. The current code is straightforward and, in the current situation, wrong.

It is time to clean up obviously sloppy code (especially since we need it to behave differently. It didn’t look so bad at the time).

    private Map<String, Object> _wrapperCache = new HashMap<String, Object>();

    ...

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;

        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = _wrapperCache.get(projects[i].getName());
            if (customProjectParent == null) {
                customProjectParent = createCustomProjectParent(projects[i]);
                _wrapperCache.put(projects[i].getName(), customProjectParent);
            }

            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }

        result = new Object[list.size()];
        list.toArray(result);

        return result;
    }

The code above checks the cache for the project wrapper. If it’s not there, create it. Return the existing or new object. This is much more efficient than what we had before, but we didn’t need it until now (that’s my story and I’m sticking to it).

Now try the use case steps.

The cat should be alive.

What Just Happened?

Oh, c’mon! Didn’t you just execute the steps right from the beginning? You asked the viewer for the expanded nodes, refreshed the viewer, and then told it which nodes to reopen.

The code to cache the wrappers was pretty simple. I am proud of how little code we continue to write.

customnavigator: 13 code files and 1 properties file
customplugin: 11 code files and 2 properties file

Bear in mind that most of the code isn’t very complicated. I’ll try harder next time.

Rock on.

Code

/**
 * Coder beware: this code is not warranted to do anything.
 * Copyright Oct 17, 2009 Carlos Valcarcel
 */
package customnavigator.navigator;

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

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;

import customplugin.natures.ProjectNature;

/**
 * @author carlos
 */
public class ContentProvider implements ITreeContentProvider, IResourceChangeListener {

    private static final Object[]   NO_CHILDREN = {};
    private Map<String, Object> _wrapperCache = new HashMap<String, Object>();
    private Viewer _viewer;

    public ContentProvider() {
        ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
     */
    @Override
    public Object[] getChildren(Object parentElement) {
        Object[] children = null;
        if (IWorkspaceRoot.class.isInstance(parentElement)) {
            IProject[] projects = ((IWorkspaceRoot)parentElement).getProjects();
            children = createCustomProjectParents(projects);
        } else if (ICustomProjectElement.class.isInstance(parentElement)) {
            children = ((ICustomProjectElement) parentElement).getChildren();
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
     */
    @Override
    public Object getParent(Object element) {
        Object parent = null;

        if (IProject.class.isInstance(element)) {
            parent = ((IProject)element).getWorkspace().getRoot();
        } else if (ICustomProjectElement.class.isInstance(element)) {
            parent = ((ICustomProjectElement)element).getParent();
        } // else parent = null if IWorkspaceRoot or anything else

        return parent;
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
     */
    @Override
    public boolean hasChildren(Object element) {
        boolean hasChildren = false;

        if (IWorkspaceRoot.class.isInstance(element)) {
            hasChildren = ((IWorkspaceRoot)element).getProjects().length > 0;
        } else if (ICustomProjectElement.class.isInstance(element)) {
            hasChildren = ((ICustomProjectElement)element).hasChildren();
        }
        // else it is not one of these so return false

        return hasChildren;
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
     */
    @Override
    public Object[] getElements(Object inputElement) {
        // This is the same as getChildren() so we will call that instead
        return getChildren(inputElement);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.IContentProvider#dispose()
     */
    @Override
    public void dispose() {
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
     */
    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        _viewer = viewer;
    }

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        TreeViewer viewer = (TreeViewer) _viewer;
        TreePath[] treePaths = viewer.getExpandedTreePaths();
        viewer.refresh();
        viewer.setExpandedTreePaths(treePaths);
    }

    private Object createCustomProjectParent(IProject parentElement) {

        Object result = null;
        try {
            if (parentElement.getNature(ProjectNature.NATURE_ID) != null) {
                result = new CustomProjectParent(parentElement);
            }
        } catch (CoreException e) {
            // Go to the next IProject
        }

        return result;
    }

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;

        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = _wrapperCache.get(projects[i].getName());
            if (customProjectParent == null) {
                customProjectParent = createCustomProjectParent(projects[i]);
                _wrapperCache.put(projects[i].getName(), customProjectParent);
            }

            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }

        result = new Object[list.size()];
        list.toArray(result);

        return result;
    }

}

References

http://www.eclipsezone.com/eclipse/forums/t107049.html

Advertisements
  1. December 3, 2009 at 8:13 am

    Hey there.

    A few of us at work are using Eclipse, and we’d like to be able to have custom icons in the tabs, based on the classname of the file.

    Basically we use a PHP MVC framework where there’s a lot of same-named files in different folders, for example, 3 x users.php, but

    * class Users_Controller{}
    * class Users_Model{}
    * class Users_View{}

    What we’d like would be the ability to override the standard icon, and instead add an icon of our choosing.

    Now, I know what it’s like with software… it seems easy until you start asking “do you need a preferences interface” to “where will the resources be stored” to “how will the file content derive the icon? regexp, prefs, ??”

    Anyway – is this the sort of job you could take on?

    Do let me know via email at dev at davestewart dot co dot uk.

    Many thanks!
    Dave

  2. JayJay
    August 14, 2010 at 4:24 pm

    I get an “Invalid thread access” exception when I try to do this.
    “resourceChanged” comes from a non-UI thread and apparently SWT doesn’t like being manipulated from a non-UI thread. So likely the update has to be decoupled via a UIJob or s.th. related, but maybe I am missing something here?

  3. polo
    May 20, 2015 at 12:25 pm

    To avoid thread error in public void resourceChanged(IResourceChangeEvent event), I make following modification:

    /*
    * (non-Javadoc)
    * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
    */
    @Override
    public void resourceChanged(IResourceChangeEvent event) {
    final TreeViewer viewer = (TreeViewer) _viewer;
    Display display = viewer.getTree().getDisplay();
    if(!display.isDisposed()){
    display.asyncExec(new Runnable(){

    @Override
    public void run() {
    if(!viewer.getTree().getDisplay().isDisposed()){
    TreePath[] treePaths = viewer.getExpandedTreePaths();
    viewer.refresh();
    viewer.setExpandedTreePaths(treePaths);
    }
    }
    });
    }

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: