Thursday, June 19, 2014

TableViewer context menus

Recently I played around with context menus on tableviewers. I wanted to track the column where the context menu was activated. As it turned out to be quite a tricky task (until you know how it is done) I would like to share my findings. Furthermore I would like to show how to attach context menus to editors without having default entries for Run As and similar actions.

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

For this tutorial I created a Plug-in Project with a simple editor extension. The editor contains a TableViewer with some sample columns. Nothing special about that. Have a look at the source code or Lars' excellent editor tutorial if you are not familiar with these steps.

Step 1: Creating the editor context menu

To enable context menus, we need to register them in our editor code. The typical snippet to do this looks as follows:
MenuManager menuManager = new MenuManager();
Menu contextMenu = menuManager.createContextMenu(table);
table.setMenu(contextMenu);
getSite().registerContextMenu(menuManager, tableViewer);
registerContextMenu() automatically searches for a menu contribution with locationURI = popup:<part.id>. In our case it would look for popup:com.codeandme.editor.sample. You may also provide a dedicated id when registering the context menu. Just make sure you provide the id without the "popup:" prefix, while the menu contribution needs the "popup:" prefix in its locationURI.

You will end up with a context menu already populated with some default entries like Run As, Compare With, Team, and some more. To get rid of them, we need to register the menu using a different site:
getEditorSite().registerContextMenu(menuManager, tableViewer, false);
The boolean parameter allows to enable/disable context menu entries for the editor input part.


Step 2: Track the active column

When we want our context menu entry to behave differently depending on the column from where it was triggered, we need to track columns. Some solutions on the web use MouseListeners, which work well for the table body, but not for the header row. A nicer solution relies on MenuDetect events:
fTableViewer.getTable().addListener(SWT.MenuDetect, this);

@Override
public void handleEvent(Event event) {
 Table table = fTableViewer.getTable();

 // calculate click offset within table area
 Point point = Display.getDefault().map(null, table, new Point(event.x, event.y));
 Rectangle clientArea = table.getClientArea();
 fHeaderArea = (clientArea.y <= point.y) && (point.y < (clientArea.y + table.getHeaderHeight()));

 ViewerCell cell = fTableViewer.getCell(point);
 if (cell != null)
  fSelectedColumnIndex = cell.getColumnIndex();

 else {
  // no cell detected, click on header
  int xOffset = point.x;
  int columnIndex = 0;
  int[] order = table.getColumnOrder();
  while ((columnIndex < table.getColumnCount()) && (xOffset > table.getColumn(order[columnIndex]).getWidth())) {
   xOffset -= table.getColumn(order[columnIndex]).getWidth();
   columnIndex++;
  }

  fSelectedColumnIndex = (columnIndex < table.getColumnCount()) ? order[columnIndex] : NO_COLUMN;
 }
}
The full helper class is available under EPL and can be downloaded from the source repository.

Step 2: Delete the active column

To provide a usage example for the TableColumnTracker we will extend our editor and allow users to delete the column under the cursor using the context menu.

The command implementation simply asks the current editor to do the job:
@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
 IWorkbenchPart part = HandlerUtil.getActivePart(event);
 if (part instanceof SampleEditor)
  ((SampleEditor) part).deleteColumn();

 return null;
}
The editor needs to install the tracker and dispose the selected column upon request:
public class SampleEditor extends EditorPart {

 private TableColumnTracker fColumnTracker;

 @Override
 public void createPartControl(Composite parent) {

  [...]

  MenuManager menuManager = new MenuManager();
  Menu contextMenu = menuManager.createContextMenu(table);
  table.setMenu(contextMenu);
  getEditorSite().registerContextMenu(menuManager, fTableViewer, false);

  fColumnTracker = new TableColumnTracker(fTableViewer);
 }

 public void deleteColumn() {
  int columnIndex = fColumnTracker.getSelectedColumnIndex();

  if (columnIndex != TableColumnTracker.NO_COLUMN) {
   fTableViewer.getTable().getColumn(columnIndex).dispose();
   fTableViewer.refresh();
  }
 }
}

Wednesday, June 11, 2014

Adding hyperlink detectors to editors

Ever tried clicking on a method name in the java editor while holding the ctrl key? Sure you have. This hyperlink functionality is extensible and in this post we will see how to do that.

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: Creating the extension

Our target will be to create a simple hyperlink whenever we detect the word "preferences" in a text editor. Upon a click the preferences dialog should pop up.

Start with a new Plug-in Project, open the Manifest Editor and switch to the Extensions tab. Now add an org.eclipse.ui.workbench.texteditor.hyperlinkDetectors extension. Provide a unique id and a nice name. The name will be visible in the preferences under General/Editors/Text Editors/Hyperlinking.

The targetId points to the type of editor we would like to create our links in. As we want to use it in all text editors use org.eclipse.ui.DefaultTextEditor here.


Step 2: Class implementation

Create a new class PreferencesHyperlinkEditor extending from AbstractHyperlinkDetector. Therefore you need to define a dependency to the org.eclipse.jface.text plug-in.
package com.codeandme.hyperlinkdetector;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;

public class PreferencesHyperlinkDetector extends AbstractHyperlinkDetector implements IHyperlinkDetector {

 private static final String PREFERENCES = "preferences";

 @Override
 public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {

  IDocument document = textViewer.getDocument();
  int offset = region.getOffset();

  // extract relevant characters
  IRegion lineRegion;
  String candidate;
  try {
   lineRegion = document.getLineInformationOfOffset(offset);
   candidate = document.get(lineRegion.getOffset(), lineRegion.getLength());
  } catch (BadLocationException ex) {
   return null;
  }

  // look for keyword
  int index = candidate.indexOf(PREFERENCES);
  if (index != -1) {

   // detect region containing keyword
   IRegion targetRegion = new Region(lineRegion.getOffset() + index, PREFERENCES.length());
   if ((targetRegion.getOffset() <= offset) && ((targetRegion.getOffset() + targetRegion.getLength()) > offset))
    // create link
    return new IHyperlink[] { new PreferencesHyperlink(targetRegion) };
  }

  return null;
 }
}
The sample implementation just extracts some text and calculates offsets within the text file. It will fail if you type"preferences" more than once per line, but as a proof of concept this should be sufficient.

We also need to provide an implementation of IHyperlink. The most important method there is open(), which is called upon the click event.
package com.codeandme.hyperlinkdetector;

import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.dialogs.PreferencesUtil;

public class PreferencesHyperlink implements IHyperlink {

 private final IRegion fUrlRegion;

 public PreferencesHyperlink(IRegion urlRegion) {
  fUrlRegion = urlRegion;
 }

 @Override
 public IRegion getHyperlinkRegion() {
  return fUrlRegion;
 }

 @Override
 public String getTypeLabel() {
  return null;
 }

 @Override
 public String getHyperlinkText() {
  return null;
 }

 @Override
 public void open() {
  PreferencesUtil.createPreferenceDialogOn(Display.getDefault().getActiveShell(), null, null, null).open();
 }
}
To test your implementation open a new text file, enter some sample text and hover over the word "preferences" while holding the ctrl key.

Wednesday, June 4, 2014

Tycho 11: Install root level features


Introduction

Do you know about root level features?

Components installed in eclipse are called installable units (IUs). These are either features or products. Now IUs might be containers for other features, creating a tree like dependency structure. Lets take a short look at the Installation Details (menu Help / About Eclipse) of our sample product from tycho tutorial 8:

We can see that there exists one root level feature Tycho Built Product which contains all the features we defined for our product. What is interesting is, that the Update... and Uninstall... buttons at the bottom are disabled when we select child features.

So in an RCP application we may only update/uninstall root level features. This means that if we want to update a sub component, we need to create a new version of our main product. For a modular application this might not be a desired behavior.

The situation changes when a user installs additional components into a running RCP application. Such features will be handled as root level features and can therefore be updated separately. So our target will be to create a base product and install our features in an additional step.

Great news is, that tycho 0.20.0 allows us to do this very easily.

Tycho Tutorials

For a list of all tycho related tutorials see Tycho Tutorials Overview

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: Identify independent features

Tycho will do all the required steps for us, we only need to identify features to be installed at root level. So open your product file using either the Text Editor or XML Editor. Locate the section with the feature definitions. Now add an installMode="root" attribute to any feature to be installed on root level.
   <features>
      <feature id="org.eclipse.e4.rcp"/>
      <feature id="org.eclipse.platform"/>
      <feature id="com.codeandme.tycho.plugin.feature" installMode="root"/>
      <feature id="com.codeandme.tycho.product.feature"/>
      <feature id="org.eclipse.help" installMode="root"/>
      <feature id="org.eclipse.emf.ecore"/>
      <feature id="org.eclipse.equinox.p2.core.feature"/>
      <feature id="org.eclipse.emf.common"/>
      <feature id="org.eclipse.equinox.p2.rcp.feature"/>
      <feature id="org.eclipse.equinox.p2.user.ui"/>
      <feature id="org.eclipse.rcp"/>
      <feature id="org.eclipse.equinox.p2.extras.feature"/>
   </features>

Make sure to update the tycho version to be used to 0.20.0 or above.

Nothing more to do, build your  product and enjoy root level features in action.