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.
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:

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.
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) { } }
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); }
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.
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/.
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.
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:

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.
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.
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); }
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.
.

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.
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:
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.
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.
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 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:

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.
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
classPass 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();
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.
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 } } } } } … }
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.
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.
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.