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).
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.
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).
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).
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.
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.
LinearUndoEnforcer
An 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.
LinearUndoViolationUserApprover
An 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.
NonLocalUndoUserApprover
An 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).