可撤销的操作

我们已经查看了为工作台提供操作的许多不同方式,但我们尚未重点介绍操作的 run() 方法的实现。该方法的机制取决于所讨论的特定操作,但将代码构造为可撤销的操作允许操作参与平台撤销和重做支持。

平台在 org.eclipse.core.commands.operations 包中提供了一个可撤销的操作框架。通过实现 run() 方法中的代码来创建 IUndoableOperation,可以使该操作可用于撤销和重做。转换操作以使用操作是简单的,但实现撤销和重做行为本身除外。

编写可撤销的操作

我们将从查看一个非常简单的示例开始。回想一下自述文件示例插件中提供的简单 ViewActionDelegate。当调用该操作时,它只启动一个通知它已执行的对话框。

public void run(org.eclipse.jface.action.IAction action) {
	MessageDialog.openInformation(view.getSite().getShell(),
		MessageUtil.getString("Readme_Editor"),  
		MessageUtil.getString("View_Action_executed")); 
}
通过使用操作,run 方法负责创建一个操作来执行以前在 run 方法中执行的工作,然后请求操作历史执行该操作,以便记住该操作,从而可以进行撤销或重做。
public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
该操作包括来自 run 方法的旧行为以及对该操作的撤销和重做。
class ReadmeOperation extends AbstractOperation {
	Shell shell;
	public ReadmeOperation(Shell shell) {
		super("Readme Operation");
		this.shell = shell;
	}
	public IStatus execute(IProgressMonitor monitor, IAdaptable info) {
		MessageDialog.openInformation(shell,
			MessageUtil.getString("Readme_Editor"),  
			MessageUtil.getString("View_Action_executed"));   
		return Status.OK_STATUS;
	}
	public IStatus undo(IProgressMonitor monitor) {
		MessageDialog.openInformation(shell,
			MessageUtil.getString("Readme_Editor"),  
			"Undoing view action"));   
		return Status.OK_STATUS;
	}
	public IStatus redo(IProgressMonitor monitor) {
		MessageDialog.openInformation(shell,
			MessageUtil.getString("Readme_Editor"),  
			"Redoing view action"));   
		return Status.OK_STATUS;
	}
}

对于简单操作来说,可能可以将所有难以处理的工作移到操作类中。在这种情况下,可能最好将以前的操作类折叠成单个参数化的操作类。该操作在它运行时仅仅是执行提供的操作。这在很大程度上是应用程序设计决策。

当某个操作启动向导时,则该操作通常是作为向导的 performFinish() 方法或向导页面的 finish() 方法的一部分创建的。转换 finish 方法以使用操作类似于转换 run 方法。该方法负责创建并执行一个完成先前联机完成的工作的操作。

操作历史

迄今为止,我们已使用了操作历史而没有真正说明它。让我们再次查看创建示例操作的代码。

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
什么是操作历史的全部含义?IOperationHistory 为跟踪所有可撤销的操作的对象定义接口。当操作历史执行操作时,它首先执行该操作,然后将它添加至撤销历史中。为此,希望执行撤销和重做操作的客户机应使用 IOperationHistory 协议。

有几种方式可以检索由应用程序使用的操作历史。最简单的方式是使用 OperationHistoryFactory

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

工作台也可以用来检索操作历史。工作台配置缺省操作历史并且提供访问它的协议。以下片段演示如何从工作台获取操作历史。

IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
一旦获取了操作历史,就可以使用它来查询撤销或重做历史,查明哪个操作是下一个即将要撤销或重做的操作,或者撤销或重做特定操作。为了接收关于对历史的更改的通知,客户机可以添加 IOperationHistoryListener。其它协议允许客户机设置对历史的限制或将对特定操作的更改通知侦听器。在我们详细查看协议之前,需要了解撤销上下文

撤销上下文

当创建操作时,将为该操作指定一个撤销上下文,该上下文描述执行原始操作时的用户上下文。撤销上下文通常依赖于发起该可撤销操作的视图或编辑器。例如,在编辑器中所作的更改通常局限于该编辑器。在这种情况下,编辑器应该创建它自己的撤销上下文并对它添加到历史中的操作指定该上下文。这样,在编辑器中执行的所有操作都被认为是局部的且是半私有的。对共享模型执行操作的编辑器或视图通常使用与它们所处理的模型相关的撤销上下文。通过使用更普通的撤销上下文,一个视图或编辑器执行的操作可以在对同一模型执行操作的另一视图或编辑器中撤销。

撤销上下文在行为上相对较简单;IUndoContext 的协议相当小。上下文的主要任务是将特定操作“标记”为该撤销上下文的从属项,以便将它与在不同撤销上下文中创建的操作区分开来。这允许操作历史跟踪已执行的所有可撤销操作的全局历史,而视图和编辑器可以通过使用撤销上下文以特定的观点来过滤历史。

撤销上下文可以由正在创建可撤销操作的插件创建,也可以通过 API 进行访问。例如,工作台提供对可用于工作台范围的操作的撤销上下文的访问权。不管用什么方法获取撤销上下文,都应该在创建操作时指定它们。以下片段显示自述文件插件的 ViewActionDelegate 如何将工作台范围的上下文指定给其操作。

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
	IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
	IUndoContext undoContext = workbench.getOperationSupport().getUndoContext();
	operation.addContext(undoContext);
	operationHistory.execute(operation, null, null);
}

究竟为何要使用撤销上下文?为何不对不同的视图和编辑器使用不同的操作历史?使用不同的操作历史将假定任何特定视图或编辑器处理自己的专用撤销历史,并假定撤销在应用程序中没有全局意义。这可能适合于某些应用程序,并且在这些情况下,每个视图或编辑器都应创建自己的不同撤销上下文。其它应用程序可能希望实现应用于所有用户操作的全局撤销,而不管它们所起源的视图或编辑器。在这种情况下,工作台上下文应由将操作添加至历史的所有插件使用。

在更复杂的应用程序中,撤销既不是绝对局部的,也不是绝对全局的。而是在撤销上下文之间有一些交叉部分。这可以通过将多个上下文指定给一个操作来实现。例如,IDE 工作台视图可以处理整个工作空间,并可以认为工作空间是它的撤销上下文。在工作空间中的特定资源上打开的编辑器可以认为其操作大部分是局部的。然而,在编辑器中执行的操作事实上对特定资源和整个工作空间可能都有影响。(这种情况的一个明显的示例是 JDT 重构支持,它允许在编辑源文件对 Java 元素进行结构上的更改)。在这些情况下,能够将两个撤销上下文添加至操作非常有用,这样就可以从编辑器本身以及使用工作空间的那些视图执行撤销。

既然已经了解了撤销上下文的作用,我们可以再次查看 IOperationHistory 的协议。以下片段用来在某个上下文上执行撤销:

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
try {
	IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// handle the exception 
}
历史将获取最近执行且具有给定上下文的操作并要求该操作自己撤销。其它协议可以用来获取上下文的整个撤销或重做历史,或用来查找将在特定上下文中撤销或重做的操作。以下片段获取将在特定上下文中撤销的操作的标签。
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(myContext).getLabel();

全局撤销上下文 IOperationHistory.GLOBAL_UNDO_CONTEXT 可用来引用全局撤销历史。即,适用于历史中的所有操作,而不考虑它们的特定上下文。以下片段将获取全局撤销历史。

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
IUndoableOperation [] undoHistory = operationHistory.getUndoHistory(IOperationHistory.GLOBAL_UNDO_CONTEXT);

无论何时使用操作历史协议来执行、撤销或重作操作,客户机都能提供一个进度监视器和执行该操作可能需要的任何其它用户界面信息。此信息将传递到该操作本身。在我们的原始示例中,自述文件操作构造了一个带 shell 参数的操作,可使用该操作打开对话框。较好的方法是将参数传递到提供执行操作所需要的任何用户界面信息的执行、撤销和重做方法,而不是在操作中存储 shell。这些参数将传递到操作本身。

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation();
	...
	operationHistory.execute(operation, null, infoAdapter);
}
infoAdapterIAdaptable,它最低限度可以提供当启动对话框时可使用的 Shell。我们的示例操作将使用此参数,如下所示:
	public IStatus execute(IProgressMonitor monitor, IAdaptable info) {
	if (info != null) {
			Shell shell = (Shell)info.getAdapter(Shell.class);
		if (shell != null) {
		MessageDialog.openInformation(shell,
		MessageUtil.getString("Readme_Editor"),  
			MessageUtil.getString("View_Action_executed"));   
				return Status.OK_STATUS;
		}
	}
	// do something else...
}

撤销和重做操作处理程序

平台提供了标准的撤销和重做可重定目标的操作处理程序,视图和编辑器可配置它们来为其特定上下文提供撤销和重做支持。当创建操作处理程序时,将为它指定上下文,以便以适合该特定视图的方式来过滤操作历史。操作处理程序负责更新撤销和重做标签以显示当前在讨论的操作、为操作历史提供适当的进度监视器和用户界面信息并在当前操作无效时有选择地修剪历史。为了方便起见,提供了一个操作组,该操作组创建操作处理程序并将它们分配到全局撤销和重做操作中。

new UndoRedoActionGroup(this.getSite(), undoContext, true);
最后一个参数是布尔值,在当前可用于撤销或重做的操作无效时,该布尔值指示是否应该除去指定上下文的撤销和重做历史。此参数的设置与提供的撤销上下文以及由具有该上下文的操作所使用的验证策略相关。

应用程序撤销模型

我们先前查看了如何使用撤销上下文来实现不同种类的应用程序撤销模型。将一个或多个上下文指定给操作的能力允许应用程序实现撤销策略,这些策略对于每个视图或编辑器是绝对局部的,在所有插件之间是绝对全局的,或者对于某个模型是介于局部和全局之间的。涉及撤销和重做的另一个设计决定是,无论是在任何时候可以撤销或重做任何操作,还是该模型是绝对线性的,仅考虑撤销或重做最近的操作。

IOperationHistory 定义允许灵活的撤销模型的协议,从而让各个实现来确定所允许的内容。到目前为止,我们所看到的撤销和重作协议都假定只有一个暗指的操作可用于在特定撤销上下文中进行撤销或重做。提供了其它协议来允许客户机执行特定操作,而不管该操作在历史中的位置。可以配置操作历史,以便可以实现适合于应用程序的模型。此操作是通过一个接口来完成的,该接口用来在撤销或重做操作之前预先核准任何撤销或重做请求。

操作核准程序

IOperationApprover 定义用于核准特定操作的撤销和重做的协议。操作核准程序安装在操作历史上。接着,特定操作核准程序可以检查所有操作的有效性、仅检查某些上下文的操作或者在操作中发现意外情况时提示用户。以下片段显示应用程序如何配置操作历史以对所有操作强制实施线性撤销模型。
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// set an approver on the history that will disallow any undo that is not the most recent operation
history.addOperationApprover(new LinearUndoEnforcer());

在这种情况下,框架提供的操作核准程序 LinearUndoEnforcer 安装在历史中,以防止对其所有撤销上下文中不是最近完成或撤销的操作执行撤销或重做。

另一操作核准程序 LinearUndoViolationUserApprover 检测相同的情况并提示用户是否应该允许该操作继续执行。可以将此操作核准程序安装在特定工作台部件上。

IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// set an approver on this part that will prompt the user when the operation is not the most recent.
IOperationApprover approver = new LinearUndoViolationUserApprover(myUndoContext, myWorkbenchPart);
history.addOperationApprover(approver);

插件开发者可以自由地开发和安装它们自己的操作核准程序以便实现特定于应用程序的撤销模型和核准策略。

撤销和 IDE 工作台

我们已经了解了使用工作台协议来访问操作历史和工作台撤销上下文的代码段。此代码段是通过使用 IWorkbenchOperationSupport(可以从工作台获得它)来实现的。工作台范围的撤销上下文的概念是相当普通的。工作台应用程序负责确定工作台撤销上下文所暗指的特定作用域,以及当提供撤销支持时哪些视图或编辑器使用该工作台上下文。

就 Eclipse IDE 工作台来说,应该将工作台撤销上下文指定给任何影响整个 IDE 工作空间的操作。此上下文由处理工作空间的视图(如资源导航器)使用。IDE 工作台将适配器安装 IUndoContext(它返回工作台撤销上下文)的工作空间上。这种基于模型的注册允许处理工作空间的插件获取适当的撤销上下文,即使这些插件是无头的且没有引用任何工作台类。

// get the operation history
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// obtain the appropriate undo context for my model
IUndoContext workspaceContext = (IUndoContext)ResourcesPlugin.getWorkspace().getAdapter(IUndoContext.class);
if (workspaceContext != null) {
	// create an operation and assign it the context
}

鼓励其它插件使用这一技巧来注册基于模型的撤销上下文。