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.

2 comments:

  1. Hy there,

    dynamic menu contributions have also been natively implemented in Kepler starting from M4, please see http://www.descher.at/descher-vu/2012/12/dynamic-menu-contributions/ for documentation!

    All the best, marco

    ReplyDelete
  2. Can't get the AbstractContributionFactory to work, createContributionItems get's never called. Seems like there is a bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=411765

    ReplyDelete