Mastering Eclipse Plug-in Development
上QQ阅读APP看书,第一时间看更新

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 (implements ILabelProvider)
  • FeedContentProvider (implements ITreeContentProvider)

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 a UIJob class or via the UISynchronizer 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.