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:
- Create a custom project.
- Open one of the nodes
- Create another custom project
- 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
- Open the customnavigator ContentProvider.
- Change resourceChanged with the following bold code:
- Add this field definition to the top of the file:
- Make the following changes to createCustomProjectParents():
- 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)
ContentProvider.java
@Override public void resourceChanged(IResourceChangeEvent event) { TreeViewer viewer = (TreeViewer) _viewer; TreePath[] treePaths = viewer.getExpandedTreePaths(); viewer.refresh(); viewer.setExpandedTreePaths(treePaths); }
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; }
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
Leave a Reply Cancel reply
Top Posts
Archives
- January 2012 (1)
- February 2011 (1)
- January 2011 (1)
- August 2010 (2)
- June 2010 (1)
- May 2010 (2)
- April 2010 (1)
- March 2010 (1)
- February 2010 (4)
- January 2010 (2)
- December 2009 (5)
- November 2009 (2)
- October 2009 (6)
- September 2009 (6)
- August 2009 (4)
- July 2009 (6)
- April 2009 (2)
- February 2009 (2)
- December 2008 (1)
- October 2008 (2)
- September 2008 (2)
- August 2008 (1)
Top Rated
Blog Stats
- 484,940 hits
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
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?
I guess I have not done enough in this example to run into that problem.
Take a look at comment #3 at Part 14 (https://cvalcarcel.wordpress.com/2009/12/13/writing-an-eclipse-plug-in-part-14-common-navigator-refactoring-the-children/). I think runtime’s solution will address your problem.
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);
}
}
});
}