Mastering Eclipse Plug-in Development

5 (1 reviews total)
By Dr Alex Blewitt
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Plugging in to JFace and the Common Navigator Framework

About this book

Mastering Eclipse Plug-in Development shows you how to build an extensible application using custom extension points and dynamic OSGi services in Eclipse. Dynamic design patterns such as whiteboard and extender are covered along with specific techniques to deal with native and legacy code.

This book dives right into the details and teaches you how to define new JFace wizards and extend Eclipse with custom extension points. Then the book moves quickly on to the details of how to define new commands for the Eclipse console and how to include native code in a plug-in. You will engage with modular application design patterns and Thread Context ClassLoaders before getting the details on how to create as well as manage P2 sites and write help documentation for an Eclipse plug-in.

Publication date:
August 2014
Publisher
Packt
Pages
362
ISBN
9781783287796

 

Chapter 1. Plugging in to JFace and the Common Navigator Framework

JFace is the set of widgets that comprise the Eclipse user interface, and it builds on top of the Standard Widget Toolkit (SWT). JFace also provides a number of standard higher-level tools that can provide interaction with users, such as wizards and standard navigator plug-ins.

In this chapter, we will create a news feed reader using a JFace wizard, and then we will contribute it to the common navigator so that it shows up in views such as the Package Explorer view.

 

JFace wizards


Whenever a new project or file is created in Eclipse, the standard JFace wizard is used. For example, the following screenshots show the wizards to create a new Plug-in Project or Java Class:

A JFace wizard has a common section at the top and bottom of the dialog, which provides the title/icon and transition buttons along with an optional help link. Each wizard consists of one or more linked pages that define the visible content area and the button bar. The window title is shared across all pages; the page title and page message allow information to be shown at the top. The page adds per-page content into the content area by exposing a page control. The wizard can be displayed with a wizard dialog or by integrating with the workbench functionality, such as the newWizards extension point. The following diagram illustrates this:

Creating a feeds wizard

A wizard is created as a subclass of Wizard or another class that implements the IWizard interface. Create a new plug-in project called com.packtpub.e4.advanced.feeds.ui and ensure that the Generate an activator and This plug-in will make contributions to the UI options are selected. Click on Finish to accept the defaults.

Creating the classes

Create a new class called com.packtpub.e4.advanced.feeds.ui.NewFeedWizard that extends org.eclipse.jface.wizard.Wizard. This creates a skeleton file with a performFinish method.

To add content, one or more pages must be created. A page is a subclass of WizardPage or another class that implements the IWizardPage interface. Pages are typically added within the constructor or addPages methods of the owning wizard.

Create a new class called com.packtpub.e4.advanced.feeds.ui.NewFeedPage that extends org.eclipse.jface.wizard.WizardPage. The default implementation will be missing a constructor; create a default constructor that passes the string "NewFeedPage" to the superclass' constructor.

The code should now look like the following code snippet:

package com.packtpub.e4.advanced.feeds.ui;
import org.eclipse.jface.wizard.Wizard;
public class NewFeedWizard extends Wizard {
  public boolean performFinish() {
    return false;
  }
}
package com.packtpub.e4.advanced.feeds.ui;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.widgets.Composite;
public class NewFeedPage extends WizardPage {
  protected NewFeedPage() {
    super("NewFeedPage");
  }
  public void createControl(Composite parent) {
  }
}

Adding pages to the wizard

The wizard has an addPages method that is called when it is about to be shown. This allows one or more pages to be added to allow the wizard to do work. For simple wizards, a single page is often enough; but for complex wizards, it may make sense to break it down into two or more individual pages. A multipage wizard typically steps through its pages in order, but more complex transitions can be achieved if necessary.

Create a new instance of NewFeedPage and assign it to an instance variable called newFeedPage. Create an addPages method that calls addPage with newFeedPage as an argument, as shown in the following code:

private NewFeedPage newFeedPage = new NewFeedPage();
public void addPages() {
  addPage(newFeedPage);
}

Adding content to the page

Each page has an associated content area, which is populated through the createControl method on the page class. This is given a Composite object to add widgets; a typical wizard page starts off with exactly the same stanza as other container methods by creating a new Composite, setting it as the control on the page and making it incomplete. The code is as follows:

public void createControl(Composite parent) {
  Composite page = new Composite(parent,SWT.NONE);
  setControl(page);
  setPageComplete(false);
}

Pages are typically set up as data gathering devices, and the logic is delegated to the wizard to decide what action to take. In this case, a feed has a simple URL and a title, so the page will store these as two instance variables and set up UI widgets to save the content.

Tip

Downloading the example code

You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

The code can also be downloaded from the book's GitHub repository at https://github.com/alblue/com.packtpub.e4.advanced/.

One of the easiest ways to get data out of the page is to persist references to the SWT Text boxes that are used to input content and then provide accessors to access the data. To guard against failure, accessor methods need to test for null and check that the widget hasn't been disposed. The code is as follows:

private Text descriptionText;
private Text urlText;
public String getDescription() {
  return getTextFrom(descriptionText);
}
private String getTextFrom(Text text) {
  return text==null || text.isDisposed() ? null : text.getText();
}
public String getURL() {
  return getTextFrom(urlText);
}

This allows the parent wizard to access the data entered by the user once the page is complete. The process of getting the data is typically performed within the performFinish method, where the resulting operation can be displayed.

The page's user interface is built in the createControl method. This is typically organized with a GridLayout, although this isn't a requirement. The user interface for wizards tend to offer a grid of Label and Text widgets, so it could look like the following code snippet:

  page.setLayout(new GridLayout(2, false));
  page.setLayoutData(new GridData(GridData.FILL_BOTH));
  Label urlLabel = new Label(page, SWT.NONE);
  urlLabel.setText("Feed URL:");
  urlText = new Text(page, SWT.BORDER);
  urlText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
  Label descriptionLabel = new Label(page, SWT.NONE);
  descriptionLabel.setText("Feed description:");
  descriptionText = new Text(page, SWT.BORDER);
  descriptionText.setLayoutData(
    new GridData(GridData.FILL_HORIZONTAL));

The Finish button on the wizard is enabled when the page is marked as complete. Each wizard knows what information is required to finish; when it is finished, it should call setPageComplete(true). This can be arranged in the NewFeedPage class by listening to text entry changes on the feed description and URL and setting the page to be complete when both have non-empty values:

private class CompleteListener implements KeyListener {
  public void keyPressed(KeyEvent e) {
  }
  public void keyReleased(KeyEvent e) {
    boolean hasDescription =
     !"".equals(getTextFrom(descriptionText));
    boolean hasUrl = !"".equals(getTextFrom(urlText));
    setPageComplete(hasDescription && hasUrl);
  }
}
public void createControl(Composite parent) {
  …
  CompleteListener listener = new CompleteListener();
  urlText.addKeyListener(listener);
  descriptionText.addKeyListener(listener);
}

Now, whenever a key is pressed and there is text present in both the description and URL fields, the Finish button will be enabled; if text is removed from either field, it will be disabled.

Testing the wizard

To test whether the wizard works as expected before it is integrated into an Eclipse application, a small standalone test script can be created. Although bad practice, it is possible to add a main method to NewFeedWizard to allow it to display the wizard in a standalone fashion.

Wizards are displayed with the JFace WizardDialog. This takes a Shell and the Wizard instance; so a simple test can be run using the following snippet of code:

public static void main(String[] args) {
  Display display = new Display();
  Shell shell = new Shell(display);
  new WizardDialog(shell, new NewFeedWizard()).open();
  display.dispose();
}

Now, if the wizard is run, a standalone shell will be displayed and the fields and checks can be tested for correct behavior. A more complex set of tests can be set up with a UI test framework such as SWTBot.

Tip

For more information about SWTBot, see chapter 9 of the book Eclipse 4 Plug-in Development by Example Beginner's Guide, Packt Publishing, or visit the SWTBot home page at http://eclipse.org/swtbot/.

Adding titles and images

If the wizard is shown as is, the title area will be empty. Typically, a user will need to know what information to put in and what is required in order to complete the dialog. Each page can contribute information specific to that step. In the case of a multipage wizard where there are several distinct stages, each page can contribute its own information.

In the case of the new feed page, the title and message can be informational. The constructor is a good place to set the initial title and message. The code to perform this operation is as follows:

protected NewFeedPage() {
  super("NewFeedPage");
  setTitle("Add New Feed");
  setMessage("Please enter a URL and description for a news feed");
}

When feed information is entered, the message can be replaced to indicate that a description or URL is required. To clear the message, invoke setMessage(null). To add an error message, invoke setMessage and pass in one of the constants from IMessageProvider, as shown:

public void keyReleased(KeyEvent e) {
  boolean hasDescription
   = !"".equals(getTextFrom(descriptionText));
  boolean hasUrl = !"".equals(getTextFrom(urlText))
  if (!hasDescription) {
    setMessage("Please enter a description"
      IMessageProvider.ERROR);
  }
  if (!hasUrl) {
    setMessage("Please enter a URL", IMessageProvider.ERROR);
  }
  if (hasDescription && hasUrl) {
    setMessage(null);
  }
  setPageComplete(hasDescription && hasUrl);
}

To display an image on the wizard as a whole, the page can have an image of size 75 x 58 pixels. This can be set from an image descriptor in the constructor:

setImageDescriptor(
 ImageDescriptor.createFromFile(
  NewFeedPage.class, "/icons/full/wizban/newfeed_wiz.png"));

Now, running the wizard will display an icon at the top-right corner (if it doesn't, check that build.properties includes the icons/ directory in the bin.includes property):

Note

Due to Eclipse bug 439695, Eclipse 4.4.0 may be unable to load the IMessageProvider.ERROR image. If the red cross is seen as a small red dot, this can be ignored; it will work when running as an Eclipse plug-in. This bug is fixed in Eclipse 4.4.1 and above, and does not occur in Eclipse 4.3.

Use this to add a feed file of http://www.packtpub.com/rss.xml with a description of Packt Publishing special offers.

Adding help

To add help, the wizard needs to declare that help is available. During the construction of the wizard or in the addPages method, a call to the parent's setHelpAvailable method with a true parameter has to be invoked.

Help is delegated to each page by calling a performHelp method. This allows context-sensitive help to be delivered for the specific page displayed, and it also helps to get the state of the page or its previous page states. The code is as follows:

// Add to the NewFeedWizard class
public void addPages() {
  addPage(new NewFeedPage());
  setHelpAvailable(true);
}
// Add to the NewFeedPage class
public void performHelp() {
  MessageDialog.openInformation(getShell(),
    "Help for Add New Feed",
    "You can add your feeds into this as an RSS or Atom feed, "
  + "and optionally specify an additional description "
  + "which will be used as the feed title.");
}

Executing the preceding code will show a Help button on the bottom of the dialog; when clicked, it will show a help dialog with some text as shown in the following screenshot:

Finishing the wizard

When the user clicks on the Finish button on the wizard, the corresponding performFinish method is called. This allows the wizard to acquire data from the underlying pages and perform whatever action is required.

In this case, a Properties file called news.feeds can be created underneath a project called bookmarks in the workspace. This will require that org.eclipse.core.resources is added to the plug-in's dependencies.

Tip

For more information about creating resources and projects, see chapter 6 of Eclipse 4 Plug-in Development by Example Beginner's Guide, Packt Publishing, or visit the Eclipse help documentation at http://help.eclipse.org.

First, acquire or create a project called bookmarks and then acquire or create a file called news.feeds. The underlying content will be stored in the Properties file as a list of key=value pairs, where key is the URL and value is the description.

To simplify access to ResourcesPlugin, create a helper method in NewFeedWizard that will obtain an IFile from a project as follows:

private IFile getFile(String project, String name,
 IProgressMonitor monitor) throws CoreException {
  IWorkspace workspace = ResourcesPlugin.getWorkspace();
  IProject bookmarks = workspace.getRoot().getProject(project);
  if (!bookmarks.exists()) {
    bookmarks.create(monitor);
  }
  if (!bookmarks.isOpen()) {
    bookmarks.open(monitor);
  }
  return bookmarks.getFile(name);
}

To access the feeds from the resources, create two public static final variables that define the name of the project and the name of the bookmarks file:

public static final String FEEDS_FILE = "news.feeds";
public static final String FEEDS_PROJECT = "bookmarks";

These can be used to create a helper method to add a single feed on the resource by reading the contents of the file (creating it if it doesn't exist), adding the feed, and then saving the new contents of the file:

private synchronized void addFeed(String url, String description)
 throws CoreException, IOException {
  Properties feeds = new Properties();
  IFile file = getFile(FEEDS_PROJECT, FEEDS_FILE, null);
  if (file.exists()) {
    feeds.load(file.getContents());
  }
  feeds.setProperty(url, description);
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  feeds.store(baos, null);
  ByteArrayInputStream bais =
   new ByteArrayInputStream(baos.toByteArray());
  if (file.exists()) {
    file.setContents(bais, true, false, null);
  } else {
    file.create(bais, true, null);
  }
}

Finally, to hook this method in with the performFinish method being called, pull the description and url fields from NewFeedPage and then pass them to the addFeed method. Since an exception may be raised, surround them with a try/catch block that returns true or false (as appropriate):

public boolean performFinish() {
  String url = newFeedPage.getURL();
  String description = newFeedPage.getDescription();
  try {
    if (url != null && description != null) {
      addFeed(url, description);
    }
    return true;
  } catch (Exception e) {
    newFeedPage.setMessage(e.toString(), IMessageProvider.ERROR);
    return false;
  }
}

Running the wizard from the test harness won't have an effect, since the workspace won't be open. It is thus necessary to contribute this to the new wizard's mechanism in Eclipse, which is done in the next section.

Adding the FeedWizard to the newWizards extension point

To integrate the wizard into Eclipse, it should be added to the newWizards extension point provided by the org.eclipse.ui plug-in.

There is a minor modification required in the wizard to make it fit in with the new wizard extension point: implementing the INewWizard interface. This adds an additional method, init, that provides the current selection at the time of calling. This allows the wizard to detect whether (for example) a string URL is selected and, if so, fills the dialog with that information. The modification is shown in the following code snippet:

public class NewFeedWizard extends Wizard implements INewWizard {
  public void init(IWorkbench workbench,
   IStructuredSelection selection) {
  }
  …
}

Add the following extension, along with a 16 x 16 icon, to the plugin.xml file:

<plugin>
  <extension point="org.eclipse.ui.newWizards">
    <category name="Feeds"
      id="com.packtpub.e4.advanced.feeds.ui.category"/>
    <wizard name="New Feed"
      class="com.packtpub.e4.advanced.feeds.ui.NewFeedWizard"
      category="com.packtpub.e4.advanced.feeds.ui.category"
      icon="icons/full/etool16/newfeed_wiz.gif"
      id="com.packtpub.e4.advanced.feeds.ui.newFeedWizard"/>
  </extension>
</plugin>

Now, the Eclipse application can be run and a Feeds category will be added to the New dialog situated under File.

Tip

Icon sizes, along with naming conventions, can be found on the Eclipse wiki at http://wiki.eclipse.org/User_Interface_Guidelines.

Adding a progress monitor

The wizard container can have a progress bar for long-running operations and can be used to display the progress, including optional cancellation, if the job requires it.

To acquire a progress monitor, the wizard's container can be used to invoke RunnableWithProgress, which is an interface that has a run method with an IProgressMonitor argument. The addFeed method can be moved into an anonymous inner class, which allows the wizard to display the progress of the operation without blocking the UI. The code is as follows:

public boolean performFinish() {
  final String url = newFeedPage.getURL();
  final String description = newFeedPage.getDescription();
  try {
    boolean fork = false;
    boolean cancel = true;
    getContainer().run(fork, cancel, new IRunnableWithProgress() {
      public void run(IProgressMonitor monitor) 
       throws InvocationTargetException, InterruptedException {
        try {
          if (url != null && description != null) {
            addFeed(url, description, monitor);
          }
        } catch (Exception e) {
          throw new InvocationTargetException(e);
        }
      }
    });
    return true;
  } catch (InvocationTargetException e) {
    newFeedPage.setMessage(e.getTargetException().toString(),
     IMessageProvider.ERROR);
    return false;
  } catch (InterruptedException e) {
    return true;
  }
}

The fork argument passed to the run method indicates whether the job should run in the path of the performFinish method or if it should run in a new thread. If a new thread is chosen, the run method will return hiding any errors that may be generated from the result of the addFeed call. The cancel argument provides an option to cancel the job if run in the same thread.

The addFeed method can be modified (as shown in the following code snippet) to interact with the progress monitor after converting it to a SubMonitor and passing it to the child tasks as appropriate. Regularly checking whether the monitor is cancelled will give the user the best experience if they decide to cancel the job.

private synchronized void addFeed(String url, String description,
 IProgressMonitor monitor) throws CoreException, IOException {
  SubMonitor subMonitor = SubMonitor.convert(monitor, 2);
  if(subMonitor.isCanceled())
    return;
  Properties feeds = new Properties();
  IFile file = getFile(FEEDS_PROJECT, FEEDS_FILE, subMonitor);
  subMonitor.worked(1);
  if (file.exists()) {
    feeds.load(file.getContents());
  }
  if(subMonitor.isCanceled())
    return;
  feeds.setProperty(url, description);
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  feeds.save(baos, null);
  ByteArrayInputStream bais = 
   new ByteArrayInputStream(baos.toByteArray());
  if(subMonitor.isCanceled())
    return;
  if (file.exists()) {
    file.setContents(bais, true, false, subMonitor);
  } else {
    file.create(bais, true, subMonitor);
  }
  subMonitor.worked(1);
  if (monitor != null) {
    monitor.done();
  }
}

If the wizard is shown now, the cancellation button and progress bars are not shown. In order to ensure that the wizard shows them, the addPages method must also declare that the progress monitor is required, as shown in the following code snippet:

public void addPages() {
  addPage(newFeedPage);
  setHelpAvailable(true);
  setNeedsProgressMonitor(true);
}

Showing a preview

When feed information is added, the Finish button is automatically enabled. However, the user may be interested in verifying whether they have entered the correct URL. Adding an additional Preview page allows the user to confirm that the right details have been entered.

To do this, create a new class called NewFeedPreviewPage that extends WizardPage. Implement it using a constructor similar to the NewFeedPage and with a createControl method that instantiates a Browser widget. Since loading a URL will be an asynchronous operation, the browser can be pre-filled with a Loading... text message that will be briefly visible before the page is loaded. The code is as follows:

public class NewFeedPreviewPage extends WizardPage {
  private Browser browser;
  protected NewFeedPreviewPage() {
    super("NewFeedPreviewPage");
    setTitle("Preview of Feed");
    setMessage("A preview of the provided URL is shown below");
    setImageDescriptor(
     ImageDescriptor.createFromFile(NewFeedPreviewPage.class,
       "/icons/full/wizban/newfeed_wiz.png"));
  }
  public void createControl(Composite parent) {
    Composite page = new Composite(parent, SWT.NONE);
    setControl(page);
    page.setLayout(new FillLayout());
    browser = new Browser(page, SWT.NONE);
    browser.setText("Loading...");
  }
}

To have the browser show the correct URL when it is shown, override the setVisible method. This only needs to be done if the page is visible and also if the browser widget is not null and not disposed.

To find out what the value of the URL should be, the previous wizard page needs to be acquired. Although it is possible to store these as static variables and use Java to pass references, the parent Wizard already has a list of these pages and can return them by name. Use this to acquire the NewFeedPage from the list of pages, from which the URL can be acquired. The resulting setVisible method then looks like the following code snippet:

  public void setVisible(boolean visible) {
    if (visible && browser != null && !browser.isDisposed()) {
      NewFeedPage newFeedPage = (NewFeedPage)
       (getWizard().getPage("NewFeedPage"));
      String url = newFeedPage.getURL();
      browser.setUrl(url);
    }
    super.setVisible(visible);
  }

The final step is to integrate this into the wizard itself. The only change that is needed here is to add a field to store a reference to the preview page and pass it in the addPages method, as shown in the following code:

private NewFeedPreviewPage newFeedPreviewPage
 = new NewFeedPreviewPage();
public void addPages() {
  addPage(newFeedPage);
  addPage(newFeedPreviewPage);
  ...
}

Now, when the wizard is invoked, both the Next and Finish buttons are enabled once the fields have been completed. Clicking on the Finish button as before will add the feed, but the Next button will take the user to a page that has a preview of the page.

.

 

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.

 

Summary


In this chapter, we covered how to create a dialog wizard with an optional page and have that drive an entry in the New Wizard dialog. This was used to create a feeds bookmark, which was then subsequently used to drive a set of fields in a common navigator—showing how the children of a resource can be updated.

In the next chapter, we will look at how Eclipse manages its extension points, and we will learn how to plug in to existing extension points as well as define custom extension points.

About the Author

  • Dr Alex Blewitt

    Dr Alex Blewitt has been developing Java applications since version 1.0 was released in 1996, and has been using the Eclipse platform since its first release as part of the IBM WebSphere Studio product suite. He got involved in the open source community as a tester when Eclipse 2.1 was being released for macOS, and then subsequently as an editor for EclipseZone, including being a finalist for Eclipse Ambassador in 2007. More recently, Alex has been writing for InfoQ, covering Java and specifically Eclipse and OSGi subjects.

    He is co-founder of the Docklands.LJC, a regional branch of the London Java Community in the Docklands, and a regular speaker at conferences.

    Alex currently works for an investment bank in London, and is a Director of Bandlem Limited. Alex blogs at https://alblue.bandlem.com and tweets as @alblue on Twitter, and is the author of both Mastering Eclipse 4 Plug-in Development, and Swift Essentials, both by Packt Publishing.

    Browse publications by this author

Latest Reviews

(1 reviews total)
Excellent

Recommended For You

Book Title
Unlock this full book FREE 10 day trial
Start Free Trial