Common navigator
The common navigator is a JFace TreeView
component that has extension points for displaying arbitrary types of objects. Instead of having to write content and label providers for all sorts of different objects, the common navigator provides a tree view that allows plug-ins to contribute different renderers based on the type of object in the tree.
The common navigator is used by the Project Explorer view in Eclipse and is used to show the graphics and labels for the packages, classes, and their methods and fields, as shown in the following screenshot. It is also used in the enterprise Java plug-in to provide Servlet and context-related information.
None of the resources shown in the screenshot of the Project Explorer view exist as individual files on disk. Instead, the Project Explorer view presents a virtual view of the web.xml
contents. The J2EEContentProvider
and J2EELabelProvider
nodes are used to expand the available content set and generate the top-level node, along with references to the underlying source files.
Note
Note that, as of Eclipse 4.4, the common navigator is an Eclipse 3.x plug-in, and as such, works with the Eclipse 3.x compatibility layer. CommonViewer
provides a JFace TreeViewer
subclass that may be suitable in standalone E4 applications. However, it resides in the same plug-in as the CommonNavigator
class that has dependencies on the Eclipse 3.x layer, and therefore may not be used in pure E4 applications.
Creating a content and label provider
The common navigator allows plug-ins to register a JFace ContentProvider
and LabelProvider
instance for components in the tree. These are then used to provide the nodes in the common navigator tree.
Tip
For more information about content providers and label providers, see chapter 3 of Eclipse 4 Plug-in Development by Example Beginner's Guide, Packt Publishing, or other tutorials on the Internet.
To provide a content view of the feed's properties file, create the following classes:
Feed
(a data object that contains a name and URL)FeedLabelProvider
(implementsILabelProvider
)FeedContentProvider
(implementsITreeContentProvider
)
The FeedLabelProvider
class needs to show the name of the feed as the label; implement the getText
method as follows:
public String getText(Object element) { if (element instanceof Feed) { return ((Feed) element).getName(); } else { return null; } }
Optionally, an image can be returned from the getImage
method. One of the default images from the Eclipse platform could be used (for example, IMG_OBJ_FILE
from the workbench's shared images). This is not required in order to implement a label provider.
The FeedContentProvider
class will be used to convert an IResource
object into an array of Feed
objects. Since the IResource
content can be loaded via a URI, it can easily be converted into a Properties
object, as shown in the following code:
private static final Object[] NO_CHILDREN = new Object[0]; public Object[] getChildren(Object parentElement) { Object[] result = NO_CHILDREN; if (parentElement instanceof IResource) { IResource resource = (IResource) parentElement; if (resource.getName().endsWith(".feeds")) { try { Properties properties = new Properties(); InputStream stream = resource.getLocationURI() .toURL().openStream(); properties.load(stream); stream.close(); result = new Object[properties.size()]; int i = 0; Iterator it = properties.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, String> entry = (Entry<String, String>) it.next(); result[i++] = new Feed(entry.getValue(), entry.getKey()); } } catch (Exception e) { return NO_CHILDREN; } } } return result; }
The getElements
method is not invoked when ITreeContentProvider
is used; but conventionally, it can be used to provide compatibility with other processes if necessary.
Integrating into Common Navigator
The providers are registered with a navigatorContent
element from the extension point org.eclipse.ui.navigator.navigatorContent
. This defines a unique ID, a name, an icon, and whether it is active by default or not. This can be created using the plug-in editor or by adding the configuration directly to the plugin.xml
file, as shown:
<extension point="org.eclipse.ui.navigator.navigatorContent"> <navigatorContent activeByDefault="true" contentProvider= "com.packtpub.e4.advanced.feeds.ui.FeedContentProvider" labelProvider= "com.packtpub.e4.advanced.feeds.ui.FeedLabelProvider" id="com.packtpub.e4.advanced.feeds.ui.feedNavigatorContent" name="Feed Navigator Content"> </navigatorContent> </extension>
Running the preceding code will cause the following error to be displayed in the error log:
Missing attribute: triggerPoints
The navigatorContent
extension, needs to be told when this particular instance should be activated. In this case, when an IResource
is selected with an extension of .feeds
, this navigator should be enabled. The configuration is as follows:
<navigatorContent ...> <triggerPoints> <and> <instanceof value="org.eclipse.core.resources.IResource"/> <test forcePluginActivation="true" property="org.eclipse.core.resources.extension" value="feeds"/> </and> </triggerPoints> </navigatorContent>
Adding the preceding code to the plugin.xml
file fixes the error. There is an additional element, possibleChildren
, which is used to assist in invoking the correct getParent
method of an element:
<possibleChildren> <or> <instanceof value="com.packtpub.e4.advanced.feeds.ui.Feed"/> </or> </possibleChildren>
The purpose of doing this is to tell the common navigator that when a Feed
instance is selected, it can defer to the FeedContentProvider
to determine the parent of a Feed
. In the current implementation, this does not change, since the getParent
method of the FeedContentProvider
returns null
.
Running the Eclipse instance at this point will fail to display any content in the Project Explorer view. To do that, the content navigator extensions need to be bound to the right viewer by its ID.
Binding content navigators to views
To prevent every content navigator extension from being applied to every view, individual bindings allow specific providers to be bound to specific views. This is not stored in the commonNavigator
extension point, as this can be a many-to-many relationship. Instead, a new extension point, org.eclipse.ui.navigator.viewer
, and a nested viewerContentBinding
point are used:
<extension point="org.eclipse.ui.navigator.viewer"> <viewerContentBinding viewerId="org.eclipse.ui.navigator.ProjectExplorer"> <includes> <contentExtension pattern= "com.packtpub.e4.advanced.feeds.ui.feedNavigatorContent"/> </includes> </viewerContentBinding> </extension>
The viewerId
declares the view for which the binding is appropriate.
Tip
A list of viewerId
values can be found from the Host OSGi Console by executing the following command:
osgi> pt -v org.eclipse.ui.views | grep id
This provides a full list of IDs contained within the declarations of the extension point org.eclipse.ui.views
. Note that not all of the IDs may be views, and most of them won't be subtypes of the CommonNavigator
view.
The pattern defined in the content extension can be a specific name (such as the one used in the example previously) or it can be a regular expression, such as com.packtpub.*
, to match all extensions in a given namespace.
Running the application now will show a list of the individual feed elements underneath news.feeds
, as shown in the following screenshot:
Adding commands to the common navigator
Adding a command to the common navigator is the same as other commands; a command
and handler
are required, followed by a menuContribution
that targets the appropriate location URI.
To add a command to show the feed in a web browser, create a ShowFeedInBrowserHandler
class that uses the platform's ability to show a web page. In order to show a web page, get hold of the PlatformUI
browser support, which offers the opportunity to create a browser and open a URL. The code is as follows:
public class ShowFeedInBrowserHandler extends AbstractHandler { public Object execute(ExecutionEvent event) throws ExecutionException { ISelection sel = HandlerUtil.getCurrentSelection(event); if (sel instanceof IStructuredSelection) { Iterator<?> it = ((IStructuredSelection)sel).iterator(); while (it.hasNext()) { Object object = it.next(); if (object instanceof Feed) { String url = ((Feed) object).getUrl(); try { PlatformUI.getWorkbench().getBrowserSupport() .createBrowser(url).openURL(new URL(url)); } catch (Exception e) { StatusManager.getManager().handle( new Status(Status.ERROR,Activator.PLUGIN_ID, "Could not open browser for " + url, e), StatusManager.LOG | StatusManager.SHOW); } } } } return null; } }
If the selection is an IStructuredSelection
, its elements will be processed; for each selected Feed
, a browser will be opened. The StatusManager
class is used to report an error to the workbench if there is a problem.
The command will need to be registered in the plugin.xml
file as follows:
<extension point="org.eclipse.ui.commands"> <command name="Show Feed in Browser" description="Shows the selected feed in browser" id="com.packtpub.e4.advanced.feeds.ui.ShowFeedInBrowserCommand" defaultHandler= "com.packtpub.e4.advanced.feeds.ui.ShowFeedInBrowserHandler"/> </extension>
To use this in a pop-up menu, it can be added as a menuContribution
(which is also done in the plugin.xml
file). To ensure that the menu is only shown if the element selected is a Feed
instance, the standard pattern for iterating over the current selection is used, as illustrated in the following code snippet:
<extension point="org.eclipse.ui.menus"> <menuContribution allPopups="false" locationURI= "popup:org.eclipse.ui.navigator.ProjectExplorer#PopupMenu"> <command style="push" commandId= "com.packtpub.e4.advanced.feeds.ui.ShowFeedInBrowserCommand"> <visibleWhen checkEnabled="false"> <with variable="selection"> <iterate ifEmpty="false" operator="or"> <adapt type="com.packtpub.e4.advanced.feeds.ui.Feed"/> </iterate> </with> </visibleWhen> </command> </menuContribution> </extension>
Tip
For more information about handlers and selections, see chapter 3 of Eclipse 4 Plug-in Development by Example Beginner's Guide, Packt Publishing, or other tutorials on the Internet.
Now, when the application is run, the Show Feed in Browser menu will be shown when the feed is selected in the common navigator, as illustrated in the following screenshot:
Reacting to updates
If the file changes, then currently the viewer does not refresh. This is problematic because additions or removals to the news.feeds
file do not result in changes in the UI.
To solve this problem, ensure that the content provider implements IResourceChangeListener
(as shown in the following code snippet), and that when initialized, it is registered with the workspace. Any resource changes will then be delivered, which can be used to update the viewer.
public class FeedContentProvider implements ITreeContentProvider, IResourceChangeListener { private Viewer viewer; public void dispose() { viewer = null; ResourcesPlugin.getWorkspace(). removeResourceChangeListener(this); } public void inputChanged(Viewer v, Object old, Object noo) { this.viewer = viewer; ResourcesPlugin.getWorkspace() .addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE); } public void resourceChanged(IResourceChangeEvent event) { if (viewer != null) { viewer.refresh(); } } }
Now when changes occur on the underling resource, the viewer will be automatically updated.
Optimizing the viewer updates
Updating the viewer whenever any resource changes is not very efficient. In addition, if a resource change is invoked outside of the UI thread, then the refresh operation will cause an Invalid Thread Access error message to be generated.
To fix this, the following two steps need to be performed:
- Invoke the
refresh
method from inside aUIJob
class or via theUISynchronizer
class - Pass the changed resource to the
refresh
method
To run the refresh
method inside a UIJob
class, replace the call with the following code:
new UIJob("RefreshingFeeds") { public IStatus runInUIThread(IProgressMonitor monitor) { if(viewer != null) { viewer.refresh(); } return Status.OK_STATUS; } }.schedule();
This will ensure the operation works correctly, regardless of how the resource change occurs.
To ensure that the viewer is only refreshed on resources that really need it, IResourceDeltaVisitor
is required. This has a visit
method which includes an IResourceDelta
object that includes the changed resources.
An inner class, FeedsRefresher
, that implements IResourceDeltaVisitor
can be used to walk the change for files matching a .feeds
extension. This ensures that the display is only updated/refreshed when a corresponding .feeds
file is updated, instead of every file. By returning true
from the visit
method, the delta
is recursively walked so that files at any level can be found. The code is as follows:
private class FeedsRefresher implements IResourceDeltaVisitor { public boolean visit(IResourceDelta delta) throws CoreException{ final IResource resource = delta.getResource(); if (resource != null && "feeds".equals(resource.getFileExtension())) { new UIJob("RefreshingFeeds") { public IStatus runInUIThread(IProgressMonitor monitor) { if(viewer != null) { viewer.refresh(); } return Status.OK_STATUS; } }.schedule(); } return true; } }
This is hooked into the feed content provider by replacing the resourceChanged
method with the following code:
public void resourceChanged(IResourceChangeEvent event) { if (viewer != null) { try { FeedsRefresher feedsChanged = new FeedsRefresher(); event.getDelta().accept(feedsChanged); } catch (CoreException e) { } } }
Although the generic viewer only has a refresh
method to refresh the entire view, StructuredViewer
has a refresh
method that takes a specific object to refresh. This allows the visit to be optimized further, as shown in the following code snippet:
new UIJob("RefreshingFeeds") { public IStatus runInUIThread(IProgressMonitor monitor) { if(viewer != null) { ((StructuredViewer)viewer).refresh(resource); } return Status.OK_STATUS; } }.schedule();
Linking selection changes
There is an option in Eclipse-based views: Link editor with selection. This allows a view to drive the selection in an editor, such as the Outline view's ability to select the appropriate method in a Java source file.
This can be added into the common navigator using a linkHelper
. To add this, open the plugin.xml
file and add the following to link the editor whenever a Feed
instance is selected:
<extension point="org.eclipse.ui.navigator.linkHelper"> <linkHelper class="com.packtpub.e4.advanced.feeds.ui.FeedLinkHelper" id="com.packtpub.e4.advanced.feeds.ui.FeedLinkHelper"> <editorInputEnablement> <instanceof value="org.eclipse.ui.IFileEditorInput"/> </editorInputEnablement> <selectionEnablement> <instanceof value="com.packtpub.e4.advanced.feeds.ui.Feed"/> </selectionEnablement> </linkHelper> </extension>
This will set up a call to the FeedLinkHelper
class that will be notified whenever the selected editor is a plain file or the object is of type Feed
.
To ensure that linkHelper
is configured for the navigator, it is necessary to add it in to the includes
element of the viewerContentBinding
point created previously, as shown in the following code:
<extension point="org.eclipse.ui.navigator.viewer"> <viewerContentBinding viewerId="org.eclipse.ui.navigator.ProjectExplorer"> <includes> <contentExtension pattern= "com.packtpub.e4.advanced.feeds.ui.feedNavigatorContent"/> <contentExtension pattern= "com.packtpub.e4.advanced.feeds.ui.FeedLinkHelper"/> </includes> </viewerContentBinding> </extension>
FeedLinkHelper
needs to implement the interface org.eclipse.ui.navigator.ILinkHelper
, which defines the two methods findSelection
and activateEditor
to convert an editor to a selection and vice versa.
Opening an editor
To open an editor and set the selection correctly, it will be necessary to include two more bundles to the project: org.eclipse.jface.text
(for the TextSelection
class) and org.eclipse.ui.ide
(for the IDE
class). This will tie the bundle into explicit availability of the IDE, but it can be marked as optional (because if there is no IDE, then there are no editors). It may also require org.eclipse.ui.navigator
to be added to include referenced class files.
To implement the activateEditor
method, it is necessary to find where the entry is inside the properties file and then set the selection appropriately. Since there is no easy way to do this, the contents of the file will be read instead (with a BufferedInputStream
instance) while searching for the bytes that make up the selected item. Because there is a hardcoded name of bookmarks
and a feed of news.feeds
, this can be used to acquire the file content; though for real applications, the Feed
object should know its parent and be able to provide that dynamically. The following code snippet shows how to set the selection appropriately:
public class FeedLinkHelper implements ILinkHelper { public void activateEditor(IWorkbenchPage page, IStructuredSelection selection) { Object object = selection.getFirstElement(); if (object instanceof Feed) { Feed feed = ((Feed) object); byte[] line = (feed.getUrl().replace(":", "\\:") + "=" + feed.getName()).getBytes(); IProject bookmarks = ResourcesPlugin.getWorkspace() .getRoot().getProject(NewFeedWizard.FEEDS_PROJECT); if (bookmarks.exists() && bookmarks.isOpen()) { IFile feeds = bookmarks.getFile(NewFeedWizard.FEEDS_FILE); if (feeds.exists()) { try { TextSelection textSelection = findContent(line,feeds); if (textSelection != null) { setSelection(page, feeds, textSelection); } } catch (Exception e) { // Ignore } } } } } … }
Finding the line
To find the content of the line, it is necessary to get the contents of the file and then perform a pass-through looking for the sequence of bytes. If the bytes are found, the start point is recorded and is used to return a TextSelection
. If they are not found, then return a null
, which indicates that the value shouldn't be set. This is illustrated in the following code snippet:
private TextSelection findContent(byte[] content, IFile file) throws CoreException, IOException { int len = content.length; int start = -1; InputStream in = new BufferedInputStream(file.getContents()); int pos = 0; while (start == -1) { int b = in.read(); if (b == -1) break; if (b == content[0]) { in.mark(len); boolean found = true; for (int i = 1; i < content.length && found; i++) { found &= in.read() == content[i]; } if (found) { start = pos; } in.reset(); } pos++; } if (start != -1) { return new TextSelection(start, len); } else { return null; } }
This takes advantage of the fact that BufferedInputStream
will perform the mark
operation on the underlying content stream and allow backtracking to occur. Because this is only triggered when the first character of the input is seen, it is not too inefficient. To further optimize it, the content could be checked for the start of a new line.
Setting the selection
Once the appropriate selection has been identified, it can be opened in an editor through the IDE
class. This provides an openEditor
method that can be used to open an editor at a particular point, from which the selection service can be used to set the text selection on the file. The code is as follows:
private void setSelection(IWorkbenchPage page, IFile feeds, TextSelection textSelection) throws PartInitException { IEditorPart editor = IDE.openEditor(page, feeds, false); editor.getEditorSite() .getSelectionProvider().setSelection(textSelection); }
Now when the element is selected in the project navigator, the corresponding news.feeds
resource will be opened as long as Link editor with selection is enabled.
The corresponding direction, linking the editor with the selection in the viewer, is much less practical. The problem is that the generic text editor won't fire the method until the document is opened, and then there are limited ways in which the cursor position can be detected from the document. More complex editors, such as the Java editor, provide a means to model the document and understand where the cursor is in relation to the methods and fields. This information is used to update the outline and other views.