Previous Page
Next Page

8.5. Editor Actions

Editor actions can appear as menu items in the editor's context menu, as toolbar buttons in the workbench's toolbar, and as menu items in the workbench's menu (see Figure 6-10 on page 244). This section covers adding actions to an editor programmatically, whereas Section 6.5, Editor Actions, on page 244 discussed adding actions by using declarations in the plug-in manifest (see Section 14.2.4, Marker resolutionquick fix, on page 520 for an example of manipulating the content in an existing text editor).

8.5.1. Context menu

Typically, editors have context menus populated by actions targeted at the editor or at selected objects within the editor. There are several steps to creating an editor's context menu and several more steps to register the editor so that others can contribute actions (see Section 6.3, Object Actions, on page 224, Section 6.5.1, Defining an editor context menu, on page 245, and Section 6.5.2, Defining an editor context action, on page 246 for information concerning how actions are contributed to an editor's context menus via the plug-in manifest).

8.5.1.1. Creating actions

The first step is to create the menu item actions that will appear in the context menu. The Properties editor needs an action that will remove the selected tree elements from the editor. In addition, this action adds a selection listener to facilitate keeping its enablement state in sync with the current tree selection.

package com.qualityeclipse.favorites.editors;

import ...

public class RemovePropertiesAction extends Action
{
   private final PropertiesEditor editor;
   private final TreeViewer viewer;

   private ISelectionChangedListener listener =
      new ISelectionChangedListener() {
      public void selectionChanged(SelectionChangedEvent e) {
         setEnabled(!e.getSelection().isEmpty());
      }
   };
   public RemovePropertiesAction(
      PropertiesEditor editor,
      TreeViewer viewer,
      String text,
      ImageDescriptor imageDescriptor
   ) {
      super(text, imageDescriptor);
      this.editor = editor;
      this.viewer = viewer;
      setEnabled(false);
      viewer.addSelectionChangedListener(listener);
  }

   public void run() {
      ISelection sel = viewer.getSelection();
      Tree tree = viewer.getTree();
      tree.setRedraw(false);
      try {
         Iterator iter = ((IStructuredSelection) sel).iterator();
         while (iter.hasNext())
            ((PropertyElement) ((Object) iter.next()))
               .removeFromParent();
      }
      finally {
         tree.setRedraw(true);
     }
   }
}

Tip

As shown in the preceding code, use the tree's setRedraw(boolean) method to reduce flashing when making more than one modification to a control or its model.


In PropertiesEditor, create a new field to hold the action and then call the following new method from createPages() method to initialize the field.

private RemovePropertiesAction removeAction;

private void createActions() {
   ImageDescriptor removeImage = PlatformUI.getWorkbench()
      .getSharedImages().getImageDescriptor(
         ISharedImages.IMG_TOOL_DELETE);
   removeAction =
      new RemovePropertiesAction(
         this, treeViewer, "Remove", removeImage);
}

This same action is used later for keyboard-based actions (see Section 8.5.2.4, Keyboard actions, on page 361) and global actions (see Section 8.5.2.1, Global actions, on page 358).

8.5.1.2. Creating the context menu

The context menu must be created at the same time as the editor. However, because contributors can add and remove menu items based on the selection, its contents cannot be determined until just after the user clicks the right mouse button and just before the menu is displayed. To accomplish this, set the menu's RemoveAllWhenShown property to true so that the menu will be built from scratch every time, and add a menu listener to dynamically build the menu. In addition, the menu must be registered with the control so that it will be displayed, and with the editor site so that other plug-ins can contribute actions to it (see Section 6.4, View Actions, on page 237).

For the Properties editor, modify createPages() to call this new createContextMenu() method:

private void createContextMenu() {
   MenuManager menuMgr = new MenuManager("#PopupMenu");
   menuMgr.setRemoveAllWhenShown(true);
   menuMgr.addMenuListener(new IMenuListener() {
      public void menuAboutToShow(IMenuManager m) {
         PropertiesEditor.this.fillContextMenu(m);
     }
   });
   Tree tree = treeViewer.getTree();
   Menu menu = menuMgr.createContextMenu(tree);
   tree.setMenu(menu);
   getSite().registerContextMenu(menuMgr,treeViewer);
}

8.5.1.3. Dynamically building the context menu

Every time a user clicks the right mouse button, the context menu's content must be rebuilt from scratch because contributors can add actions based on the editor's selection. In addition, the context menu must contain a separator with the IWorkbenchActionConstants.MB_ADDITIONS constant, indicating where those contributed actions will appear in the context menu. The createContextMenu() method (see the previous section) calls this new fillContextMenu(IMenuManager) method:

private void fillContextMenu(IMenuManager menuMgr) {
   boolean isEmpty = treeViewer.getSelection().isEmpty();
   removeAction.setEnabled(!isEmpty);
   menuMgr.add(removeAction);
   menuMgr.add(
      new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
}

When this functionality is in place, the context menu, containing the Remove menu item plus items contributed by others, will appear (see Figure 8-8).

Figure 8-8. The Properties editor's context menu.


8.5.2. Editor contributor

An instance of org.eclipse.ui.IEditorActionBarContributor manages the installation and removal of global menus, menu items, and toolbar buttons for one or more editors. The manifest specifies which contributor, typically a subclass of org.eclipse.ui.part.EditorActionBarContributor or org.eclipse.ui.part.MultiPageEditorActionBarContributor, is associated with which editor type (see Section 8.1, Editor Declaration, on page 326). The platform then sends the following events to the contributor, indicating when an editor has become active or inactive, so that the contributor can install or remove menus and buttons as appropriate.

dispose()This method is automatically called when the contributor is no longer needed. It cleans up any platform resources, such as images, clipboard, and so on, which were created by this class. This follows the if you create it, you destroy it theme that runs throughout Eclipse.

init(IActionBars, IWorkbenchPage)This method is called when the contributor is first created.

setActiveEditor(IEditorPart)This method is called when an associated editor becomes active or inactive. The contributor should insert and remove menus and toolbar buttons as appropriate.

The EditorActionBarContributor class implements the IEditorActionBarContributor interface, caches the action bar and workbench page, and provides two new accessor methods.

getActionBars()Returns the contributor's action bars provided to the contributor when it was initialized.

getPage()Returns the contributor's workbench page provided to the contributor when it was initialized.

The MultiPageEditorActionBarContributor class extends EditorActionBarContributor, providing a new method to override instead of the setActiveEditor(IEditorPart) method.

setActivePage(IEditorPart)Sets the active page of the multipage editor to the given editor. If there is no active page, or if the active page does not have a corresponding editor, the argument is null.

8.5.2.1. Global actions

By borrowing from org.eclipse.ui.editors.text.TextEditorActionContributor and org.eclipse.ui.texteditor.BasicTextEditor-ActionContributor, you will create your own contributor for the Properties editor. This contributor hooks up global actions (e.g., cut, copy, paste, etc. in the Edit menu) appropriate not only to the active editor but also to the active page within the editor.

package com.qualityeclipse.favorites.editors;

import ...

public class PropertiesEditorContributor
   extends EditorActionBarContributor
{
   private static final String[] WORKBENCH_ACTION_IDS = {
      ActionFactory.DELETE.getId(),
      ActionFactory.UNDO.getId(),
      ActionFactory.REDO.getId(),
      ActionFactory.CUT.getId(),
      ActionFactory.COPY.getId(),
      ActionFactory.PASTE.getId(),
      ActionFactory.SELECT_ALL.getId(),
      ActionFactory.FIND.getId(),
      IDEActionFactory.BOOKMARK.getId(),
   };
   private static final String[] TEXTEDITOR_ACTION_IDS = {
      ActionFactory.DELETE.getId(),
      ActionFactory.UNDO.getId(),
      ActionFactory.REDO.getId(),
      ActionFactory.CUT.getId(),
      ActionFactory.COPY.getId(),
      ActionFactory.PASTE.getId(),
      ActionFactory.SELECT_ALL.getId(),
      ActionFactory.FIND.getId(),
      IDEActionFactory.BOOKMARK.getId(),
   };

   public void setActiveEditor(IEditorPart part) {
      PropertiesEditor editor = (PropertiesEditor) part;
      setActivePage(editor, editor.getActivePage());
  }
   public void setActivePage(
      PropertiesEditor editor,
      int pageIndex
   ) {
     IActionBars actionBars = getActionBars();
     if (actionBars != null) {
        switch (pageIndex) {
           case 0 :
              hookGlobalTreeActions(editor, actionBars);
              break;
           case 1 :
             hookGlobalTextActions(editor, actionBars);
             break;
        }
        actionBars.updateActionBars();
   }
  }

  private void hookGlobalTreeActions(
     PropertiesEditor editor,
     IActionBars actionBars
  ) {
     for (int i = 0; i < WORKBENCH_ACTION_IDS.length; i++)
        actionBars.setGlobalActionHandler(
          WORKBENCH_ACTION_IDS[i],
          editor.getTreeAction(WORKBENCH_ACTION_IDS[i]));
  }

  private void hookGlobalTextActions(
     PropertiesEditor editor,
     IActionBars actionBars
  ) {
     ITextEditor textEditor = editor.getSourceEditor();
     for (int i = 0; i < WORKBENCH_ACTION_IDS.length; i++)
        actionBars.setGlobalActionHandler(
           WORKBENCH_ACTION_IDS[i],
           textEditor.getAction(TEXTEDITOR_ACTION_IDS[i]));
  }
}

Now modify the Properties editor to add accessor methods for the contributor.

public ITextEditor getSourceEditor() {
   return textEditor;
}

public IAction getTreeAction(String workbenchActionId) {
   if (ActionFactory.DELETE.getId().equals(workbenchActionId))
      return removeAction;
   return null;
}

Append the following lines to the pageChange() method to notify the contributor when the page has changed so that the contributor can update the menu items and toolbar buttons appropriately.

IEditorActionBarContributor contributor =
   getEditorSite().getActionBarContributor();
if (contributor instanceof PropertiesEditorContributor)
   ((PropertiesEditorContributor) contributor)
      .setActivePage(this, newPageIndex);

8.5.2.2. Top-level menu

Next, add the remove action to a top-level menu for the purpose of showing how it is accomplished. In this case, instead of referencing the action directly as done with the context menu (see Section 8.5.1, Context menu, on page 354), you will use an instance of org.eclipse.ui.actions. RetargetAction, or more specifically, org.eclipse.ui.actions. LabelRetargetAction, which references the remove action indirectly via its identifier. You'll be using the ActionFactory.DELETE.getId() identifier, but could use any identifier so long as setGlobalActionHandler(String, IAction) is used to associate the identifier with the action. To accomplish all this, add the following to the PropertiesEditorContributor.

private LabelRetargetAction retargetRemoveAction =
   new LabelRetargetAction(ActionFactory.DELETE.getId(), "Remove");

public void init(IActionBars bars, IWorkbenchPage page) {
   super.init(bars, page);
   page.addPartListener(retargetRemoveAction);
}

public void contributeToMenu(IMenuManager menuManager) {
   IMenuManager menu = new MenuManager("Property Editor");
   menuManager.prependToGroup(
      IWorkbenchActionConstants.MB_ADDITIONS,
      menu);
   menu.add(retargetRemoveAction);
}

public void dispose() {
   getPage().removePartListener(retargetRemoveAction);
   super.dispose();
}

Once in place, this code causes a new top-level menu to appear in the workbench's menu bar (see Figure 8-9).

Figure 8-9. Property Editor menu.


8.5.2.3. Toolbar buttons

You can use the same retargeted action (see previous section) to add a button to the workbench's toolbar by including the following code in PropertiesEditorContributor.

public void contributeToToolBar(IToolBarManager manager) {
    manager.add(new Separator());
    manager.add(retargetRemoveAction);
}

8.5.2.4. Keyboard actions

By using the remove action again (see Section 8.5.1.1, Creating actions, on page 354), you can hook in the Delete key by modifying the initTreeEditors() method introduced earlier (see Section 8.3.5, Editing versus selecting, on page 349) so that when a user presses it, the selected property key/value pairs in the tree will be removed.

private void initTreeEditors() {
  ... existing code ...
  treeViewer.getTree().addKeyListener(new KeyListener() {
     public void keyPressed(KeyEvent e) {
        if (e.keyCode == SWT.ALT)
           isAltPressed = true;
        if (e.character == SWT.DEL)
           removeAction.run();
     }
     public void keyReleased(KeyEvent e) {
       if (e.keyCode == SWT.ALT)
          isAltPressed = false;
    }
  });
}

8.5.3. Undo/Redo

Adding the capability for a user to undo and redo actions involves separating user edits into actions visible in the user interface and the underlying operations that can be executed, undone, and redone. Typically each action will instantiate a new operation every time the user triggers that action. The action gathers the current application state, such as the currently selected elements, and the operation caches that state so that it can be executed, undone and redone independent of the original action. An instance of IOperationHistory manages the operations in the global undo/redo stack (see Figure 8.10). Each operation uses one or more associated undo/redo contexts to keep operations for one part separate from operations for another.

Figure 8-10. The Eclipse undo/redo infrastructure


In this case, you need to split the RemovePropertiesAction (see Section 8.5.1.1, Creating actions, on page 354), moving some functionality into a new RemovePropertiesOperation class. The AbstractOperation superclass implements much of the required IUndoableOperation interface.

public class RemovePropertiesOperation extends AbstractOperation
{
   private final TreeViewer viewer;
   private final PropertyElement[] elements;

   public RemovePropertiesOperation(
      TreeViewer viewer, PropertyElement[] elements
  )  {
     super(getLabelFor(elements));
     this.viewer = viewer;
     this.elements = elements;
  }

The constructor calls the getLabelFor() method to generate a human-readable label for the operation based on the currently selected elements. This label appears wherever the undo/redo actions appears such as on the Edit menu.

private static String getLabelFor(PropertyElement[] elements) {
   if (elements.length == 1) {
      PropertyElement first = elements[0];
      if (first instanceof PropertyEntry) {
         PropertyEntry propEntry = (PropertyEntry) first;
         return "Remove property " + propEntry.getKey();
      }
      if (first instanceof PropertyCategory) {
         PropertyCategory propCat = (PropertyCategory) first;
         return "Remove category " + propCat.getName();
      }
   }
   return "Remove properties";
}

The execute() method prompts the user to confirm the operation and removes the specified properties. If the info argument is not null, then it can be queried for a UI context in which to prompt the user for information during execution. If the monitor argument is not null, then it can be used to provide progress feedback to the user during execution. This method is only called the first time the operation is executed.

public IStatus execute(IProgressMonitor monitor, IAdaptable info)
   throws ExecutionException
{
    // If a UI context has been provided,
    // then prompt the user to confirm the operation.

   if (info != null) {
      Shell shell = (Shell) info.getAdapter(Shell.class);
      if (shell != null) {
         if (!MessageDialog.openQuestion(
            shell,
            "Remove properties",
           "Do you want to remove the currently selected properties?"
         ))
            return Status.CANCEL_STATUS;
      }
   }

   // Perform the operation.

   return redo(monitor, info);
}

The execute() method calls the redo() method to perform the actual property removal. This method records information about the elements being removed in two additional fields so that this operation can be undone. The arguments passed to the redo() method are identical to those supplied to the execute() method described before.

private PropertyElement[] parents;
private int[] indexes;

public IStatus redo(IProgressMonitor monitor, IAdaptable info)
   throws ExecutionException
{
   // Perform the operation, providing feedback to the user
   // through the progress monitor if one is provided.

   parents = new PropertyElement[elements.length];
   indexes = new int[elements.length];

   if (monitor != null)
      monitor.beginTask("Remove properties", elements.length);

   Tree tree = viewer.getTree();
   tree.setRedraw(false);
   try {
      for (int i = elements.length; --i >= 0;) {
         parents[i] = elements[i].getParent();
         PropertyElement[] children = parents[i].getChildren();
         for (int index = 0; index < children.length; index++) {
            if (children[index] == elements[i]) {
               indexes[i] = index;
               break;
            }
         }
         elements[i].removeFromParent();

         if (monitor != null)
            monitor.worked(1);
       }
   }
   finally {
      tree.setRedraw(true);
   }

   if (monitor != null)
      monitor.done();

    return Status.OK_STATUS;
}

The undo() method reverses the current operation by reinserting the removed elements into the model.

public IStatus undo(IProgressMonitor monitor, IAdaptable info)
   throws ExecutionException
{
   Tree tree = viewer.getTree();
   tree.setRedraw(false);
   try {
      for (int i = 0; i < elements.length; i++) {
         if (parents[i] instanceof PropertyCategory)
            ((PropertyCategory) parents[i]).addEntry(indexes[i],
                  (PropertyEntry) elements[i]);
         else
            ((PropertyFile) parents[i]).addCategory(indexes[i],
                  (PropertyCategory) elements[i]);
      }
   }
   finally {
      tree.setRedraw(true);
   }
   return Status.OK_STATUS;
}

The preceding undo() method inserts elements back into the model at exactly the same position from where they were removed. This necessitates some refactoring of the PropertyCategory addEntry() method (see Section 8.2.3, Editor model, on page 335 for more on the editor model).

public void addEntry(PropertyEntry entry) {
   addEntry(entries.size(), entry);
}

public void addEntry(int index, PropertyEntry entry) {
   if (!entries.contains(entry)) {
      entries.add(index, entry);
      ((PropertyFile) getParent()).entryAdded(
         this, entry);
   }
}

Here is a similar refactoring of the PropertyFile addCategory() method.

public void addCategory(PropertyCategory category) {
   addCategory(categories.size(), category);
}

public void addCategory(int index, PropertyCategory category) {
   if (!categories.contains(category)) {
      categories.add(index, category);
      categoryAdded(category);
   }
}

Rather than removing the selected properties, the RemovePropertiesAction must now build an array of properties to be removed and then pass that to a new instance of RemovePropertiesOperation. The operation is passed to the editor's undo/redo manager for execution along with a UI context for prompting the user and a progress monitor for user feedback. If there is an exception during execution, you could use a ExceptionsDetailsDialog (see Section 11.1.9, Details dialog, on page 420) rather than the following MessageDialog.

public void run() {

   // Build an array of properties to be removed.
   IStructuredSelection sel =
       (IStructuredSelection) viewer.getSelection();
   Iterator iter = sel.iterator();
   int size = sel.size();
   PropertyElement[] elements = new PropertyElement[size];
   for (int i = 0; i < size; i++)
      elements[i] = (PropertyElement) ((Object) iter.next());

   // Build the operation to be performed.
   RemovePropertiesOperation op =
      new RemovePropertiesOperation(viewer, elements);
   op.addContext(editor.getUndoContext());

   // The progress monitor so the operation can inform the user.
   IProgressMonitor monitor = editor.getEditorSite().getActionBars()
         .getStatusLineManager().getProgressMonitor();

    // An adapter for providing UI context to the operation.
    IAdaptable info = new IAdaptable() {
       public Object getAdapter(Class adapter) {
          if (Shell.class.equals(adapter))
             return editor.getSite().getShell();
          return null;
       }
  };

  // Execute the operation.
  try {
     editor.getOperationHistory().execute(op, monitor, info);
  }
  catch (ExecutionException e) {
     MessageDialog.openError(
        editor.getSite().getShell(),
        "Remove Properties Error",
        "Exception while removing properties: " + e.getMessage());
  }
}

The preceding run() method calls some new methods in PropertiesEditor.

public IOperationHistory getOperationHistory() {

  // The workbench provides its own undo/redo manager
  //return PlatformUI.getWorkbench()
  //   .getOperationSupport().getOperationHistory();

  // which, in this case, is the same as the default undo manager
  return OperationHistoryFactory.getOperationHistory();
}

public IUndoContext getUndoContext() {

  // For workbench-wide operations, we should return
  //return PlatformUI.getWorkbench()
  //   .getOperationSupport().getUndoContext();

  // but our operations are all local, so return our own content
  return undoContext;
}

This undoContext must be initialized along with undo and redo actions in the createActions() method (see Section 8.5.1.1, Creating actions, on page 354 for the original createActions() method).

private UndoActionHandler undoAction;
private RedoActionHandler redoAction;
private IUndoContext undoContext;

private void createActions() {
   undoContext = new ObjectUndoContext(this);
   undoAction = new UndoActionHandler(getSite(), undoContext);
   redoAction = new RedoActionHandler(getSite(), undoContext);
   ... existing code ...
}

These new undo and redo actions should appear in the context menu, so modify the fillContextMenu() method.

private void fillContextMenu(IMenuManager menuMgr) {
   menuMgr.add(undoAction);
   menuMgr.add(redoAction);
   menuMgr.add(new Separator());
   ... existing code ...
}

Then, modify the gettreeAction() method to return the new undo and redo actions so that they will be hooked to the global undo and redo actions that appear in the Edit menu.

public IAction getTreeAction(String workbenchActionId) {
   if (ActionFactory.UNDO.getId().equals(workbenchActionId))
      return undoAction;
   if (ActionFactory.REDO.getId().equals(workbenchActionId))
      return redoAction;
   if (ActionFactory.DELETE.getId().equals(workbenchActionId))
      return removeAction;
  return null;
}

Finally, the undo/redo stack for the Source page is separate from the undo/redo stack for the Properties page, so add the following line to the pageChange() method to clear the undo/redo stack when the page changes.

getOperationHistory().dispose(undoContext, true, true, false);

A better solution similar to what has already been discussed, but not implemented here, would be to merge the two undo/redo stacks into a single unified undo/redo stack shared between both the Properties and Source pages.

If operations share a common undo context but also have some contexts that are not shared, then there exists the possibility that operations from one context will be undone in a linear fashion; however, some operations from another context may be skipped. To allieviate this problem, you can register an instance of IOperationApprover to ensure that an operation will not be undone without all prior operations being undone first.

This interface also provides a way to confirm undo and redo operations that affect contexts outside the active editor and not immediately apparent to the user. The Eclipse platform contains the following subclasses of IOperationApprover useful when managing undo/redo operations with overlapping contexts.

LinearUndoEnforcerAn operation approver that enforces a strict linear undo. It does not allow the undo or redo of any operation that is not the latest available operation in all of its undo contexts.

LinearUndoViolationUserApproverAn operation approver that prompts the user to see whether linear undo violations are permitted. A linear undo violation is detected when an operation being undone or redone shares an undo context with another operation appearing more recently in the history.

NonLocalUndoUserApproverAn operation approver that prompts the user to see whether a nonlocal undo should proceed inside an editor. A non-local undo is detected when an operation being undone or redone affects elements other than those described by the editor itself.

The Eclipse SDK contains a basic undo/redo example as part of the eclipse-examples-3.1.2-win32.zip download. It provides additional undo/redo code not covered here such as an UndoHistoryView and an implementation of IOperationApprover for approving the undo or redo of a particular operation within an operation history.

8.5.4. Clipboard actions

Clipboard-based actions for an editor are identical to their respective view-based operations (see Section 7.3.7, Clipboard actions, on page 290).


Previous Page
Next Page