Thursday, June 13, 2013

Using ANTLR in your RCP

ANTLR is a parser generator widely used to create DSLs and all kinds of parsers for special purposes. In this tutorial I will show how to create a simple parser and how to implement it into an RCP.

Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.

Prerequisites

We need some stuff from the ANTLR download page. Get Complete ANTLR 4.0 Java binaries jar and ANTLR 4.0 Java runtime binaries jar from there. The full package includes the build tool for grammars, the runtime binaries are needed to execute a parser in our RCP later on.

Step 1: Create a simple grammar

As this tutorial is not about ANTLR itself, but its integration into an RCP, we will use a very simple grammar to parse CSV files (actually just a single line of separated integers).

Currently there seems to be no eclipse support for ANTLRv4. To write grammars you could use ANTLRWorks 2 or write it directly in an editor of your choice.

Create a new Plug-in Project named com.example.antlr.parser. Afterwards create a grammar file CSV.g4 in the project root and add following content:
grammar CSV;

csv: NUMBER (DELIMITER NUMBER)*;

DELIMITER: ',';
NUMBER: DIGIT+;

fragment
DIGIT: [0-9];
Step 2: Compile grammar

To compile our grammar file we need the Complete ANTLR package we downloaded earlier and a simple script. As there is no native eclipse builder available, we will use an external build script.

Create a new folder build and copy antlr-4.0-complete.jar to it. Afterwards create a new file build.bat with following content:
"C:\Program Files\Java\jdk1.7.0_04\bin\java.exe" -cp antlr-4.0-complete.jar org.antlr.v4.Tool -visitor -package com.example.antlr.gen -o ..\src\com\example\antlr\gen\foo ..\CSV.g4

Extend this to your needs, port it to ANT or integrate it in a maven build (there seems to exist a maven target for ANTLRv4). It seems that the -o parameter needs some dummy folder name at the end. At least for me the very last path entry was skipped by the build tool.

You may launch this batch file from the External Tools located in the Run menu. The launcher is available in the source repository in the build folder.


Step 3: Add ANTLR runtime

The created files show lots of error markers because we have no runtime available yet. Create a new Plug-in from Existing JAR Archives and add antlr-runtime-4.0.jar to it. Name the project org.antlr.v4.runtime and add it as a dependency to com.example.antlr.parser.  All the error markers should be gone by now.

ANTLR comes with a BSD like license so you have to check if this fits to your application needs. According to this wiki entry it seems that it fits to EPL.

Step 4: Implement the parser

To run the parser we need 2 more classes. Lets start with com.example.antlr.CSVVisitor
package com.example.antlr;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.antlr.v4.runtime.tree.RuleNode;
import org.antlr.v4.runtime.tree.TerminalNode;

import com.example.antlr.gen.CSVBaseVisitor;
import com.example.antlr.gen.CSVLexer;

public class CSVVisitor extends CSVBaseVisitor<List<Integer>> {

 @Override
 public List<Integer> visitChildren(RuleNode ctx) {
  List<Integer> numbers = new LinkedList<Integer>();
  for (int index = 0; index < ctx.getChildCount(); index++)
   numbers.addAll(visit(ctx.getChild(index)));

  return numbers;
 }

 @Override
 public List<Integer> visitTerminal(TerminalNode node) {

  if (node.getSymbol().getType() == CSVLexer.NUMBER) {
   int number = Integer.parseInt(node.getText());
   return Arrays.asList(number);
  }

  return Collections.emptyList();
 }
}
Now we can finally call the parser from com.example.antlr.CSVProcessor
package com.example.antlr;

import java.util.List;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;

import com.example.antlr.gen.CSVLexer;
import com.example.antlr.gen.CSVParser;

public class CSVProcessor {

 public static List<Integer> parseCSV(final String data) {
  CSVLexer lexer = new CSVLexer(new ANTLRInputStream(data));
  CSVParser parser = new CSVParser(new CommonTokenStream(lexer));

  return parser.csv().accept(new CSVVisitor());
    }
 
 public static void main(String[] args) {
  List<Integer> numbers = parseCSV("2,45,66,123,24,6,7,7,8");
  System.out.println(numbers);
 }
}
If you need support on ANTLRv4 check out the webpage or - even better - support the project and buy the book.

Monday, April 22, 2013

Re-using core expressions for own extension points

Most of us use the expression framework to enable/disable handlers or to display/hide UI elements by providing enabledWhen or visibleWhen statements. Recently I needed to define an enablement for a custom extension point and found it appealing to reuse the expression framework for that purpose.

Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.
 
Step 1: Create an extension point

Create a new Plug-in project com.example.expressions. Switch to the Extension Points tab and create a new extension point by clicking the Add... button.

Set Extension Point ID to sample, Extension Point Name to Sample and hit Finish. We end up in the extension point schema editor where we switch to the Definition tab.

Create a baseElement and an expression element (hit New Element for that purpose). The baseElement contains a sequence (or choice, doesn't matter) of expressions. Make sure that exactly one expression can be defined by setting Max Occurrences to 1. If you set Min Occurrences to 0 the expression is optional, otherwise it is mandatory.

An expression is constructed by elements of certain types. When you are familiar with enabledWhen, visibleWhen, ... you are already familiar with these. They are defined in another scheme which we need to reference from our custom theme. Unless bug 384001 is fixed we need to do this in the Source view of our extension. Add following line within the schema node of your extension source:
<include schemaLocation="schema://org.eclipse.core.expressions/schema/expressionLanguage.exsd"/>
Now switch back to the Definition tab and add a choice of following elements to the expression element:

not, or, and, instanceof, test, systemTest, equals, count, with, resolve, adapt, iterate, reference.

Finally add a choice (or sequence) of baseElements to the extension element. Your assembled definition should look like this:


Here is the xml definition for your reference:
<?xml version='1.0' encoding='UTF-8'?>
<!-- Schema file written by PDE -->
<schema targetNamespace="com.example.expressions" xmlns="http://www.w3.org/2001/XMLSchema">
<annotation>
      <appinfo>
         <meta.schema plugin="com.example.expressions" id="sample" name="Sample"/>
      </appinfo>
      <documentation>
         [Enter description of this extension point.]
      </documentation>
   </annotation>

   <include schemaLocation="schema://org.eclipse.core.expressions/schema/expressionLanguage.exsd"/>

   <element name="extension">
      <annotation>
         <appinfo>
            <meta.element />
         </appinfo>
      </annotation>
      <complexType>
         <choice minOccurs="1" maxOccurs="unbounded">
            <element ref="baseElement"/>
         </choice>
         <attribute name="point" type="string" use="required">
            <annotation>
               <documentation>
                  
               </documentation>
            </annotation>
         </attribute>
         <attribute name="id" type="string">
            <annotation>
               <documentation>
                  
               </documentation>
            </annotation>
         </attribute>
         <attribute name="name" type="string">
            <annotation>
               <documentation>
                  
               </documentation>
               <appinfo>
                  <meta.attribute translatable="true"/>
               </appinfo>
            </annotation>
         </attribute>
      </complexType>
   </element>

   <element name="baseElement">
      <complexType>
         <sequence>
            <element ref="expression"/>
         </sequence>
      </complexType>
   </element>

   <element name="expression">
      <complexType>
         <choice>
            <element ref="not"/>
            <element ref="or"/>
            <element ref="and"/>
            <element ref="instanceof"/>
            <element ref="test"/>
            <element ref="systemTest"/>
            <element ref="equals"/>
            <element ref="count"/>
            <element ref="with"/>
            <element ref="resolve"/>
            <element ref="adapt"/>
            <element ref="iterate"/>
            <element ref="reference"/>
         </choice>
      </complexType>
   </element>

   <annotation>
      <appinfo>
         <meta.section type="since"/>
      </appinfo>
      <documentation>
         [Enter the first release in which this extension point appears.]
      </documentation>
   </annotation>

   <annotation>
      <appinfo>
         <meta.section type="examples"/>
      </appinfo>
      <documentation>
         [Enter extension point usage example here.]
      </documentation>
   </annotation>

   <annotation>
      <appinfo>
         <meta.section type="apiinfo"/>
      </appinfo>
      <documentation>
         [Enter API information here.]
      </documentation>
   </annotation>

   <annotation>
      <appinfo>
         <meta.section type="implementation"/>
      </appinfo>
      <documentation>
         [Enter information about supplied implementation of this extension point.]
      </documentation>
   </annotation>
</schema>

Step 2: Using the extension point

Go back to the plugin.xml of com.example.expressions and switch to the Extensions tab. Add a new extension of type com.example.expressions.sample. Now add a baseElement to it and fill the expression child node with some useful content:


This simple expression example expects the current selection to be empty.

I am using the org.eclipse.ui.startup extension point to create some sample test code to evaluate the expression.

Step 3: Create Java Expression from plugin definition

When parsing extension points data to java code, we need to convert the expression definition to a java org.eclipse.core.expressions.Expression instance. The ExpressionConverter class will do that for us:
private static final String PLUGIN_ID = "com.example.expressions";
private static final String EXTENSION_POINT = "sample";
private static final String NODE_EXPRESSION = "expression";

public Expression loadExpression() {
 IConfigurationElement[] elements = Platform.getExtensionRegistry().getExtensionPoint(PLUGIN_ID, EXTENSION_POINT).getConfigurationElements();
 for (IConfigurationElement element : elements) {

  // find expression nodes
  final IConfigurationElement[] children = element.getChildren(NODE_EXPRESSION);
  if (children.length == 1) {
   // we expect exactly 1 expression node...

   final IConfigurationElement[] expressionRoot = children[0].getChildren();
   if (expressionRoot.length > 0) {
    // ... containing exactly one root entry
    try {
     return ExpressionConverter.getDefault().perform(expressionRoot[0]);

    } catch (final CoreException e1) {
    }
   }
  }
 }

 return null;
}

Step 4: Evaluating an expression

For evaluation an expression needs an IEvaluationContext. A context contains variables (which can be used by a with node of an expression) to be evaluated. If you do not need variables you can simply create a new EvaluationContext and add your own variables to it:
EvaluationContext context = new EvaluationContext(null, new Object());
context.addVariable("someName","some arbitrary content");
EvaluationContext takes as first argument a base context (which may be null) and as second argument a default variable to perform the test upon (used when no when node exists in the expression).
You can also use the default context used by the workbench. Get it from the IHandlerService:
IHandlerService service = (IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class);
IEvaluationContext currentState = service.getCurrentState();
Now evaluating the expression works as follows:
public Object evaluateExpression(Expression expression) throws CoreException {
 IHandlerService service = (IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class);
 IEvaluationContext currentState = service.getCurrentState();

 return expression.evaluate(currentState);
}
The sample project contains running code to see this expression in action.

References

Eclipse API documentation

Thursday, April 4, 2013

Adding JavaDoc to help system

Sometimes it is useful to provide JavaDoc documentation to the user. While adding such documentation to an RCP application might be a special case it could make more sense to have such documentation ready for Eclipse extensions. If you combine this with custom doclets you might generate even more than just API documentation.

Instead of adding external html files we will add this documentation to the Eclipse help system.

Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.

Step 1: Create JavaDoc

First lets create a place to store our generated files in. So create a new Plug-in Project called com.example.javadoc.

Now select the menu entry Project / Generate Javadoc....

You need to define the javadoc executable, the source files to be parsed and the destination where to create html files. We will set the latter to com.example.javadoc/javadoc.


On the last wizard page you have the option to save all settings to an ant script so you can regenerate your documentation easily.

 

After finishing the wizard you are asked whether to update the javadoc location or not. Say No To All here.

When you refresh your project in the navigator you should see the generated files.

Step 2: Optimize build process

Refreshing by hand is nasty, why not let Eclipse do the job for us? Select your generate_javadoc.xml, right click and select Run As / Ant Build ... On the Refresh tab you can adjust which resources to refresh after the build process.

Step 3: Adding files to Eclipse help

There exist lots of tutorials on how to add help content to eclipse. As a starting point take a look at Contributing a little help or Lars' tutorial on Adding Help to RCP Applications.

We will create a book with a single section containing our help content. Create the book by adding a new file help/book.xml to our project:
<?xml version="1.0" encoding="UTF-8"?>
<toc label="Sample Book">
 <topic label="API Documentation">
  <link toc="help/API_documentation.xml" />
 </topic>
</toc>
Now add another file help/API_documentation.xml:
<?xml version="1.0" encoding="UTF-8"?>
<toc label="Sample API">
 <topic label="JavaDoc" href="javadoc/index.html" />
</toc>

You can open/edit these files with the Table of Contents Editor, which gives you a nicer UI to create such files, but usually the XML is simple enough to remain in the text editor.

To add these files to Eclipse help we need to use extension points. Open your MANIFEST.MF file and switch to the Extensions tab. Create a new extension for org.eclipse.help.toc and create two toc child nodes. Each of them refers to one of our xml files. To create a new book (which is displayed as a root level element in the help contents) we need to set primary to true.


The final plugin.xml should look like this:
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.help.toc">
      <toc
            file="help/book.xml"
            primary="true">
      </toc>
      <toc
            file="help/API_documentation.xml"
            primary="false">
      </toc>
   </extension>
</plugin>
Finally switch to the Build tab and make sure help and javadoc folders are included in the Binary Build.

Run your application and select Help / Help Contents to see our new book in action.

Optional: Open a help page programmatically

The help system allows to open dedicated pages on demand. You can open an html page by calling
PlatformUI.getWorkbench().getHelpSystem().displayHelpResource("/<plugin ID>/path/to/your/file.html");

Further improvements

Currently the creation of JavaDoc is still a manual process. But we could combine this with the Custom Builder tutorial and let the builder run javadoc. Then you could add the builder to all your projects that contribute to your javadoc files.

Wednesday, February 27, 2013

Enhance your navigator by decorating resources

Is your project explorer stuffed with too many projects? Are you looking for a way to get more structure into this? I was suffering from this problem for a long time now, having workspaces with 50+ projects in it. I am using Working Sets and Mylyn, still I sometimes get lost within all my resources.

Resource Decorator Plugin

So I came up with a nice little plugin that allows to add custom decorations to your resources. Eg you could set a dedicated background color for all your projects that fit to a dedicated name pattern. You could also set the foreground color for all your src folders or highlight plugin.xml files.


The Resource Decoration is accessible through the Navigator context menu, which contains some quick decorations and a generic wizard that allows to fine tune your settings. There you can build generic filters by creating a tree structure. The Resource filter will match a resource path within your workspace. This will always be an exact match. The RegExp filter takes a regular expression to match it against the resource name. You have to use drag and drop to build your filters, DEL will remove a selected filter along with its children.


To alter decorations you can go to Preferences / General / Appearance / Resource Decorator. Take care that the Resource Decorator is enabled in Preferences / General / Appearance / Label Decorations (which should be the default setting).


The tool is at a very early stage as the interface is a little rough and filtering is a bit limited so far. Yet it is working, save and quite useful to me.

Installation

When you are interested in this little helper you can install it from

http://codeandme.googlecode.com/svn/trunk/at.pontesegger/at.pontesegger.resourcedecorator.releng.p2/target/repository

Please use the bugtracker at googlecode to report any issues or additional ideas.

The whole project is available under EPL, source code is located at Google code.

Tuesday, February 19, 2013

Using git flow in eclipse

Git flow is a great way to improve your development process. It helps in setting up dedicated branches for development, releases and fixes. If you are not familiar with git flow please read a successful git branching model before continuing. Git flow is extremely easy to use and really worth a try.

Git flow is currently not integrated in eclipse (see bug 348610), so we need to use a command line to use it. Basically I love the power of command lines but when using an IDE I prefer visual support, so lets find a way to make this a bit more user friendly.

For this tutorial I assume you are using windows (if you were on linux you would love your bash anyway).

Step 1: Installing msysgit

As we need to enter commands on the command line we need to install the necessary tools for that, no way to avoid that.

Download and install the latest version of Git. At the time of writing this is v1.8.1.2. Go with the default settings when asked for the components and select Run Git from the Windows Command Prompt when asked for Adjusting your PATH environment. Line endings can be configured as you prefer.

You need to log off so your PATH variable will be updated accordingly. Afterwards you should be able to use the git command in a console.

Step 2: Installing git flow

Now we need to install the git flow addon. Here is a short excerpt of the install wiki page:

We need 2 additional files to download and copy to our msysgit/bin folder:
Go to the util-linux-ng download site and download the Binaries zip. From there extract only getopt.exe. Then download Dependencies zip to extraxt libintl3.dll.

During the setup we need to create a temporary directory. Open a command shell and switch to your temp directory
cd temp
git clone --recursive git://github.com/nvie/gitflow.git
cd gitflow
contrib\msysgit-install.cmd
The install command should work if you are using the default install locatin for msysgit. If it still cannot find msysgit you have to provide the install location as a parameter.

Afterwards try to run gitflow by entering
git flow
in a shell. If it works you can safely remove your temporary install folder.

Step 3: Using git flow

We are ready to use git flow by now from our shell. So give it a try, best following the blog post by Jeff Kreeftmeijer: Why aren't you using git-flow?

Step 4: Adding a shell to eclipse

WickedShell is a nice plugin for eclipse that integrates a shell into the IDE. Install it and restart your IDE. Now you can open a new view Wicked Shell / Shell which basically gives you a command prompt on windows. While you can use the windows shell I would rather go with bash.

To activate it go to Preferences / Wicked Shell and select Cygwin - Bash as Default Static Shell. To make it work you need to provide your msysgit install directory under Shell root directory (do not provide the bin dir, but its parent).

You can change the Active Shell in the view menu of the Shell view.

You may see some strange characters on the prompt by now. These are control codes which cannot be displayed by the SWT widget (at least not the way wicked shell is implemented). So we need to change the prompt. Find your home directory (enter: cd ~;pwd) and create a .bashrc file with following content:
PS1='\w $ '
You should not exchange the $ sign at the end as it is used as command delimiter token by wicked shell. If you go with some other character, wicked shell will automatically insert a $ when you type your first character.

Considerations

WickedShell seems to have its limitations. So it is not possible to send a CTRL-C in a shell. I also experienced problems when trying to enter text a command prompts for (therefore the git flow init command does not work). Fortunately you need only single line commands once git flow is initialized correctly. If you need a full shell you could still Open an external Shell from the Shell view menu.

I tested WickedShell on Windows 7, according to the bugtracker there seems to be no support for Windows 8 yet.

Tuesday, January 22, 2013

Static and dynamic pulldown menus

Having pulldown menus (like the run button) is a nice way to keep your toolbars compact and tidy. In this tutorial I will explain how to add dynamic content to such menus.

Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.


Prerequisites

We start with a simple Plug-in Project named com.example.dynamicpulldown. This project contains a single view with id set to com.example.dynamicpulldown.view. Remember that views need a category to be visible via Window/Show View menu. The view implementation is not relevant for our example, so I leave this up to you.

Create a new toolbar for your view and add a single command to it.The handler implementation is -again - of no importance here.

You may download the startup project for your convenience.

Step 1: Creating a pulldown with static elements

Creating static entries is addressed in the Eclipse Wiki and covered in more detail on vogella.com. So I will keep this repetition rather short.

Switch to your plugin.xml, find your command element within your toolbar definition and change the style attribute from push to pulldown. This will give us a nice down arrow next to our toolbar element. To register a menu for our pulldown the command needs to provide an id. Set it to com.example.dynamicpulldown.baseCommand.pulldown.

Now we can register a new menuContribution and set its locationURI to menu:com.example.dynamicpulldown.baseCommand.pulldown. You can fill this menu like any other menu/toolbar with static elements.


Step 2: Creating dynamic elements

Dynamic entries are best implemented by declaring a dynamic ContributionItem for our menu. Right click on your menuContribution and add a new dynamic node. The element needs some unique id and an implementation. Set class to com.example.dynamicpulldown.commands.DynamicContributionItem and implement it.

package com.example.dynamicpulldown.commands;

import org.eclipse.jface.action.IContributionItem;
import org.eclipse.ui.actions.CompoundContributionItem;
import org.eclipse.ui.menus.CommandContributionItem;
import org.eclipse.ui.menus.CommandContributionItemParameter;
import org.eclipse.ui.menus.IWorkbenchContribution;
import org.eclipse.ui.services.IServiceLocator;

public class DynamicContributionItem extends CompoundContributionItem implements IWorkbenchContribution {

 private IServiceLocator mServiceLocator;
 private long mLastTimeStamp = 0;

 public DynamicContributionItem() {
 }

 public DynamicContributionItem(final String id) {
  super(id);
 }

 @Override
 protected IContributionItem[] getContributionItems() {

  mLastTimeStamp = System.currentTimeMillis();

  final CommandContributionItemParameter contributionParameter = new CommandContributionItemParameter(mServiceLocator, null, "org.eclipse.ui.edit.cut",
    CommandContributionItem.STYLE_PUSH);
  contributionParameter.label = "Cut " + System.currentTimeMillis();
  contributionParameter.visibleEnabled = true;

  return new IContributionItem[] { new CommandContributionItem(contributionParameter) };
 }

 @Override
 public void initialize(final IServiceLocator serviceLocator) {
  mServiceLocator = serviceLocator;
 }

 @Override
 public boolean isDirty() {
  return mLastTimeStamp + 5000 < System.currentTimeMillis();
 }
}
We are extending CompoundContributionItem as we might want to add more than one single entry. Depending on the result of isDirty() the contribution items are repopulated when the pulldown is opened. The default implementation will always trigger a refresh. Implementing IWorkbenchContribution is needed to get a serviceLocator for our dynamic elements.

Alternative: Using Contribution factories for dynamic content

Sometimes it is not possible to define a dynamic element in your plugin.xml. This could be the case when the menu itself was created dynamically. In such cases we can use a ContributionFactory. Create a new class com.example.dynamicpulldown.commands.DynamicContributionFactory.

package com.example.dynamicpulldown.commands;

import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.menus.AbstractContributionFactory;
import org.eclipse.ui.menus.CommandContributionItem;
import org.eclipse.ui.menus.CommandContributionItemParameter;
import org.eclipse.ui.menus.IContributionRoot;
import org.eclipse.ui.menus.IMenuService;
import org.eclipse.ui.services.IServiceLocator;

public class DynamicContributionFactory extends AbstractContributionFactory {

 public static void attachToMenu(final String contributionID) {
  final DynamicContributionFactory contributionFactory = new DynamicContributionFactory("menu:" + contributionID,
    null);

  final IMenuService menuService = (IMenuService) PlatformUI.getWorkbench().getService(IMenuService.class);
  menuService.addContributionFactory(contributionFactory);
 }

 public DynamicContributionFactory(final String location, final String namespace) {
  super(location, namespace);
 }

 @Override
 public void createContributionItems(final IServiceLocator serviceLocator, final IContributionRoot additions) {
  // NPE on shutdown: https://bugs.eclipse.org/bugs/show_bug.cgi?id=377119
  final CommandContributionItemParameter contributionParameter = new CommandContributionItemParameter(
    serviceLocator, null, "org.eclipse.ui.edit.cut", CommandContributionItem.STYLE_PUSH);
  contributionParameter.label = "Cut (factory)";
  contributionParameter.visibleEnabled = true;

  additions.addContributionItem(new CommandContributionItem(contributionParameter), null);
 }
}
We need to add a dependency to org.eclipse.core.expressions to make the compiler happy.

Our factory needs to be registered and attached to a certain contributionID. The static attachToMenu() method will take care of that. Only thing left is to call that method when our view is initialized:

public class DynamicPulldownView extends ViewPart {

 ...

 @Override
 public void createPartControl(Composite parent) {
  DynamicContributionFactory.attachToMenu("com.example.dynamicpulldown.baseCommand.pulldown");
 }

 ...

}
Run your application to see the dynamic content:

Considerations

The 2nd method suffers from some limitations:
  • you might see a NPE on shutdown. I think this is related to bug 377119, which should be fixed in Juno, SR2
  • The ContributionFactory is only queried once for its dynamic elements. You will not be able to update those elements when the pulldown is reopened.

Thursday, January 17, 2013

A closer look at decorators

Decorators are widely used throughout the eclipse platform. No wonder we want to use them for our projects too. As long as we stick to icon overlays decorators are really simple to use. When we want to add text decorations things get a bit more complicated.

Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.

Preparations: Initial project

As a starting point I use a simple view with a TreeViewer. You can grab the initial source from my repository.
Run the plug-in com.example.decorators as an eclipse application and activate the Code & Me/Decorator Sample View to see that everything works.


Step 1: A simple icon decorator

To add an overlay icon decorator open you plugin.xml file and switch to the Extensions tab. Add a new extension of type org.eclipse.ui.decorators. The decorator needs its unique id and a nice label.

Decorators may be enabled/disabled by the user through Preferences/General/Appearance/Label Decorations. The state flag indicates the initial enablement of our decorator.

Set lightweight to true as we have a declarative decorator. Finally provide an icon and a location where the icon should be placed regarding the base image. Unfortunately you cannot use platform:/plugin/... style paths for your icon until bug 232171 is resolved. Instead you have to copy it to your own plug-in. The icon should be exactly 7x8 pixels (for overlays).


As you run your application you can see that your decorator is applied to the Project Explorer (if you created any projects there) but not to our sample view. To enable decorators within a viewer we need to adapt the LabelProvider. Open SampleView.java and change createPartControl to:

 @Override
 public void createPartControl(Composite parent) {

  TreeViewer treeViewer = new TreeViewer(parent, SWT.BORDER);
  tree = treeViewer.getTree();
  treeViewer.setContentProvider(new SampleTreeContentProvider());

  SampleTreeLabelProvider baseLabelprovider = new SampleTreeLabelProvider();
  ILabelDecorator decorator = PlatformUI.getWorkbench().getDecoratorManager().getLabelDecorator();
  treeViewer.setLabelProvider(new DecoratingLabelProvider(baseLabelprovider, decorator));
                ... 

Run your application again to see the decorator in action.


Step 2: Enablements for decorators

Our decorator will be visible for all viewers in our RCP. Typically that is not what we want. To bind a decorator to certain types we use enablements. To bind our decorator to String objects within viewers we can use:
   <extension
         point="org.eclipse.ui.decorators">
      <decorator
            icon="images/warning_ovr.gif"
            id="com.example.decorators.decorator"
            label="Demo decorator"
            lightweight="true"
            location="TOP_RIGHT"
            state="true">
         <enablement>
            <objectClass
                  name="java.lang.String">
            </objectClass>
         </enablement>
      </decorator>
   </extension>
I am sure you will create more powerful enablements than I did.

Step 3: Text decorations

If we want to add text to viewer elements (like the team provider plug-ins do) we need to create an implementation of ILightweightLabelDecorator. Create a new class com.example.decorators.DemoDecorator

package com.example.decorators;

import org.eclipse.jface.viewers.IDecoration;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ILightweightLabelDecorator;

public class DemoDecorator implements ILightweightLabelDecorator {

 @Override
 public void decorate(Object element, IDecoration decoration) {
  decoration.addSuffix(" [decorated]");
 }

 @Override
 public void addListener(ILabelProviderListener listener) {
 }

 @Override
 public void dispose() {
  // nothing to do
 }

 @Override
 public boolean isLabelProperty(Object element, String property) {
  return false;
 }

 @Override
 public void removeListener(ILabelProviderListener listener) {
 }
}
and add the class to your decorator definition in plugin.xml. When you run your code you will see the suffix added to each element in our sample view. Unfortunately the overlay image is gone. By providing a class taking care of the decoration our declarative parameters will not be honored anymore.

To get back our overlay, we need to add it programmatically by adding/updating our decorator:
 private static final ImageDescriptor WARNING;

 static {
  WARNING = AbstractUIPlugin.imageDescriptorFromPlugin("com.example.decorators", "images/warning_ovr.gif"); 
 }

 @Override
 public void decorate(Object element, IDecoration decoration) {
  decoration.addSuffix(" [decorated]");
  decoration.addOverlay(WARNING, IDecoration.BOTTOM_RIGHT);
 }

Alternatively you could extend LightWeightDecorator, a default implementation of an ILightweightLabelDecorator that honors delarative image settings. Get it from my repository, it is available under EPL.

Step 4: Text decorations with different color

Our text suffix is rendered using the same color as the main text. If you remove the decorator enablement, you can see that the decorated text looks differently in the Project Explorer. The color used can be adjusted in Preferences/General/Appearance/Colors and Fonts under Basic/Decoration color.

To achieve the same behavior we need to use the same code as NavigatorDecoratingLabelProvider. Unfortunately it is an internal class, so access is discouraged. Our best option is to copy over the code and put it into our own class. Create a new class DecoratedLabelProvider and copy over the code from NavigatorDecoratingLabelProvider. Do not forget to adjust the name of the constructor.

Afterwards we can use our new provider in our SampleView:
 @Override
 public void createPartControl(Composite parent) {

  TreeViewer treeViewer = new TreeViewer(parent, SWT.BORDER);
  tree = treeViewer.getTree();
  treeViewer.setContentProvider(new SampleTreeContentProvider());

  SampleTreeLabelProvider baseLabelprovider = new SampleTreeLabelProvider();
  ILabelDecorator decorator = PlatformUI.getWorkbench().getDecoratorManager().getLabelDecorator();
  treeViewer.setLabelProvider(new DecoratedLabelProvider(baseLabelprovider));

Step 5: Using custom colors & fonts

To add custom colors to our main label text we can extend SampleTreeLabelProvider and implement IColorProvider (code contains only the changed parts):
public class SampleTreeLabelProvider extends LabelProvider implements IColorProvider{

 @Override
 public Color getForeground(Object element) {
  return Display.getDefault().getSystemColor(SWT.COLOR_DARK_BLUE);
 }

 @Override
 public Color getBackground(Object element) {
  return Display.getDefault().getSystemColor(SWT.COLOR_YELLOW);
 }

For custom fonts let SampleTreeLabelProvider implement IFontProvider:
public class SampleTreeLabelProvider extends LabelProvider implements IFontProvider{

 @Override
 public Font getFont(Object element) {
  return new Font(Display.getDefault(), new FontData("Arial", 12, SWT.BOLD));
 }
}
You have to use DecoratedLabelProvider to make custom fonts and colors visible.


Remember to use registries for frequently used system resources like fonts or colors.

Further reading

Check out Understanding Decorators in Eclipse and the FAQ topics.