Opérations annulables

Nous avons jusqu'à présent abordé différentes manières d'ajouter des actions au plan de travail, mais nous ne nous sommes pas intéressés à la méthode run() d'implémentation d'une action. Le mécanisme de cette méthode dépend de l'action en question, mais la structuration du code en opération annulable permet à l'action d'être prise en charge par la plateforme annuler et rétablir.

La plateforme fournit un cadre des opérations annulables dans le package org.eclipse.core.commands.operations. En implémentant le code dans une méthode run() pour créer une IUndoableOperation (Opération annulable d'interface utilisateur), l'opération peut être annulée ou rétablie. Hormis l'implémentation du comportement annuler et rétablir, la conversion d'une action pour qu'elle puisse utiliser ces opérations est simple.

Ecrire une opération annulable

Commençons par un exemple très simple. Il vous suffit de rappeler ViewActionDelegate fourni dans le plug-in d'exemple readme. Lorsqu'elle est appelée, l'action ouvre simplement une boîte de dialogue qui annonce son exécution.

public void run(org.eclipse.jface.action.IAction action) {
	MessageDialog.openInformation(view.getSite().getShell(),
		MessageUtil.getString("Readme_Editor"),  
		MessageUtil.getString("View_Action_executed")); 
}
Lorsqu'elle utilise des opérations, la méthode run se charge de la création d'une opération qui exécute le travail qu'elle a auparavant traité et de la demande d'exécution de l'opération par un historique d'opérations, de sorte qu'on s'en souvienne pour annuler et rétablir.
public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
L'opération encapsule l'ancien comportement de la méthode run ainsi que l'annulation et le rétablissement de l'opération.
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;
	}
}

Pour les actions simples, il est possible de déplacer tous les demi-cadratins et de boulonner le travail dans la classe d'opération. Dans ce cas, il est approprié de réduire les premières classes d'action à une seule classe d'action paramétrée. L'action exécutera simplement l'opération fournie au moment voulu. Ceci est largement une décision de conception d'application.

Lorsqu'une action lance un assistant, l'opération est généralement créée comme une partie intégrante de la méthode performFinish() de l'assistant ou d'une méthode finish() de la page de l'assistant. La conversion d'une méthode finish pour utiliser ces opérations est similaire à la conversion d'une méthode run. La méthode se charge de la création et de l'exécution d'une opération qui effectue le travail auparavant réalisé en ligne.

Historique des opérations

Jusqu'à présent, nous avons utilisé l'historique des opérations sans véritablement l'expliquer. Penchons-nous à nouveau sur le code qui a créé notre exemple d'opération.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
A quoi sert l'historique des opérations? IOperationHistory définit l'interface des objets qui garde une trace de toutes les opérations annulables. Après avoir exécuté une opération, il l'ajoute à l'historique des annulations. Les clients souhaitant annuler et rétablir des opérations peuvent le faire en utilisant le protocole IOperationHistory.

L'historique des opérations utilisé par une application peut être extrait de plusieurs façons. La plus simple consiste à utiliser OperationHistoryFactory.

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

Le plan de travail peut également être utilisé pour extraire l'historique des opérations. La plan de travail configure l'historique par défaut des opérations et fournit également le protocole pour y accéder. Le fragment suivant montre comment obtenir l'historique des opérations à partir du plan de travail.

IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
Une fois l'historique des opérations obtenu, vous pouvez l'utiliser pour demander un historique des annulations et des rétablissements, savoir quelle opération est la prochaine sur la liste des annulations ou des rétablissements, ou encore annuler ou rétablir des opérations. Les clients peuvent ajouter un IOperationHistoryListener afin de recevoir des notifications sur les modifications réalisées dans l'historique. Un autre protocole leur permet de fixer des limites à l'historique ou encore d'indiquer au programme d'écoute les modifications apportées à une opération en particulier. Avant d'étudier plus en détail le protocole, il nous faut comprendre le contexte d'annulation.

Contextes d'annulation

Lorsqu'une opération est créée, un contexte d'annulation lui est attribué qui décrit le contexte utilisateur dans lequel l'opération d'origine a été traitée. Le contexte d'annulation dépend généralement de la vue ou de l'éditeur qui est à l'origine de l'opération annulable. Par exemple, les modifications effectuées dans un éditeur sont souvent locales pour cet éditeur. Dans ce cas, l'éditeur doit créer son propre contexte d'annulation et assigner ce contexte aux opérations qu'il ajoute dans l'historique. De cette façon, toutes les opérations réalisées dans l'éditeur sont considérées comme locales et semi-privées. Les éditeurs ou les vues qui fonctionnent sur un modèle partagé utilisent souvent un contexte d'annulation lié au modèle qu'ils manipulent. En utilisant un contexte d'annulation plus général, les opérations réalisées par une vue ou un éditeur peuvent être annulées dans une autre vue ou un autre éditeur fonctionnant sur le même modèle.

Le comportement des contextes d'annulation est relativement simple ; le protocole de IUndoContext est assez minimal. Le rôle principal d'un contexte est de "baliser" une opération particulière comme appartenant à ce contexte d'annulation, afin de la distinguer des opérations créées dans des contextes d'annulation différents. Cela permet à l'historique des opérations de garder une trace de l'historique global de toutes les opérations annulables exécutées, tandis que les vues et éditeurs filtrent l'historique d'un point de vue spécifique grâce au contexte d'annulation.

Les contextes d'annulation peuvent être créés par le plug-in qui crée les opérations annulables, ou bien être accédés par l'API. Par exemple, le plan de travail fournit l'accès à un contexte d'annulation pouvant être utilisé pour des opérations à l'échelle du plan de travail. Quoique déjà obtenus, les contextes d'annulation doivent être attribués lors de la création d'une opération. Le fragment suivant montre comment le ViewActionDelegate du plug-in readme peut attribuer à ses opérations un contexte à l'échelle du plan de travail.

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);
}

Pourquoi utiliser les contextes d'annulation ? Pourquoi ne pas utiliser d'historiques des opérations différents pour les différentes vues et les différents éditeurs ? L'utilisation d'historiques des opérations différents suppose que toute vue ou tout éditeur gère son propre historique des annulations, et que les annulations n'aient pas de sens global dans l'application. Cette solution peut convenir à certaines applications, et dans ce cas chaque vue ou éditeur doit créer son propre contexte d'annulation. Il se peut que d'autres applications veuillent implémenter une annulation globale s'appliquant à toutes les opérations utilisateur, sans tenir compte de leur vue ou éditeur d'origine. Dans ce cas, le contexte du plan de travail doit être utilisé par tous les plug-ins ajoutant des opérations à l'historique.

Dans des applications plus complexes, l'annulation n'est ni strictement locale, ni strictement globale. Il existe ainsi quelques connexions entre les contextes d'annulation. Cela est rendu possible par l'attribution de plusieurs contextes à une même opération. Par exemple, une vue du plan de travail IDE peut utiliser l'ensemble de l'espace de travail et considérer celui-ci comme son contexte d'annulation. Un éditeur ouvert sur une ressource particulière de l'espace de travail peut considérer ses opérations comme étant principalement locales. Toutefois, les opérations réalisées dans l'éditeur peuvent en fait affecter non seulement la ressource mais également l'ensemble de l'espace de travail. (Un bon exemple de ce cas est la prise en charge de la restructuration JDT, qui permet d'apporter des changements structurels à un élément Java pendant l'édition du fichier source). Dans ces cas-là, il est utile de pouvoir ajouter les deux contextes d'annulation à l'opération afin que l'annulation puisse être réalisée à partir de l'éditeur lui-même, ainsi que des vues utilisant l'espace de travail.

Maintenant que nous avons compris la fonction d'un contexte d'annulation, nous pouvons revenir sur le protocole de IOperationHistory. Le fragment suivant est utilisé pour réaliser une annulation dans certains contextes :

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
try {
	IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// handle the exception 
}
L'historique obtiendra l'opération la plus récente présentant le contexte donné et lui demandera de s'annuler. D'autres protocoles peuvent être utilisés pour obtenir l'historique complet des annulations et des rétablissements d'un contexte, ou encore connaître l'opération qui sera annulée ou rétablie dans un contexte donné. Le fragment suivant obtient le libellé de l'opération qui sera annulée dans un contexte particulier.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(myContext).getLabel();

Le contexte d'annulation global, IOperationHistory.GLOBAL_UNDO_CONTEXT, peut être utilisé pour faire référence à l'historique d'annulation global. A savoir, à toutes les opérations de l'historique, quelques soient leur contexte particulier. Le fragment suivant obtient l'historique d'annulation global.

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

A chaque fois qu'une opération est exécutée, annulée ou rétablie à l'aide d'un protocole d'historique des opérations, les clients peuvent ajouter un moniteur de progression et des informations d'interface utilisateur supplémentaires pouvant s'avérer utiles pour l'exécution de l'opération. Ces informations sont directement transmises à l'opération. Dans notre premier exemple, l'action readme créait une opération à l'aide d'un paramètre d'interpréteur de commandes (Shell) pouvant être utilisé pour ouvrir la boîte de dialogue. Plutôt que de stocker l'interpréteur de commandes dans l'opération, une meilleure approche est de transmettre les paramètres aux méthodes exécuter, annuler et rétablir, qui fournissent toutes les informations interface utilisateur nécessaires à l'exécution de l'opération. Ces paramètres seront directement transmis à l'opération.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation();
	...
	operationHistory.execute(operation, null, infoAdapter);
}
L'infoAdapter est un IAdaptable qui fournit seulement le Shell pouvant être utilisé lors du lancement des boîtes de dialogue. L'exemple d'opération qui suit utilise ce paramètre :
	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...
}

Gestionnaires des actions annuler et rétablir

La plateforme fournit des gestionnaires d'actions reciblables standard pour les opérations annuler et rétablir. Ces gestionnaires peuvent être configurés par les vues ou éditeurs afin de permettre la prise en charge des annulations et rétablissements dans ce contexte particulier. Lors de la création d'un gestionnaire d'action, un contexte lui est attribué afin que l'historique des opérations soit filtré d'une façon appropriée à cette vue. Les gestionnaires d'actions se chargent de la mise à jour des libellés d'annulation et de rétablissement pour afficher l'opération en cours, de l'apport du moniteur de progression approprié et des informations de l'interface utilisateur à l'historique des opérations, et enfin de l'élagage de l'historique lorsque l'opération en cours n'est pas valide. Par commodité, un groupe d'actions permettant de créer les gestionnaires d'actions et de les assigner aux annulations et aux rétablissements est fourni.

new UndoRedoActionGroup(this.getSite(), undoContext, true);
Le dernier paramètre est un booléen indiquant si les historiques d'annulations et de rétablissements du contexte défini sont supprimés lorsque l'opération susceptible d'être annulée ou rétablie n'est pas valide. La configuration de ce paramètre est liée au contexte d'annulation défini et à la stratégie de validation utilisée par les opérations dans ce contexte.

Modèles d'annulation des applications

Nous avons auparavant étudié comment les contextes d'annulation peuvent être utilisés afin d'implémenter différentes sortes de modèles d'annulation des applications. La capacité d'assigner un ou plusieurs contextes aux opérations permet aux applications d'implémenter des stratégies d'annulation strictement locales à chaque vue ou éditeur, strictement globales à l'ensemble des plug-ins ou bien concernant quelques modèles intermédiaires. Il convient également de décider lors de la conception si toutes les opérations peuvent être annulées ou rétablies à tout moment, ou bien si le modèle est strictement linéaire et tient seulement compte des opérations d'annulation ou de rétablissement les plus récentes.

IOperationHistory définit le protocole permettant d'obtenir des modèles d'annulation flexible, laissant aux implémentations personnelles le soin de déterminer ce qui est autorisé. Le protocole d'annulation et de rétablissement que nous avons vu jusqu'à présent suppose qu'il n'y ait qu'une seule opération susceptible d'être annulée ou rétablie dans un contexte d'annulation défini. Un protocole additionnel est fourni afin de permettre aux clients d'exécuter une opération spécifique, sans tenir compte de sa position dans l'historique. L'historique des opérations peut être configuré de manière à ce que le modèle approprié à une application puisse être implémenté. Il suffit pour cela d'utiliser une interface pour préapprouver toute demande d'annulation ou de rétablissement avant que l'action ne soit annulée ou rétablie.

Valideur d'opérations

IOperationApprover définit le protocole de validation des annulations et rétablissements pour une opération donnée. Le valideur d'opérations est installé sur l'historique des opérations. Des approbateurs d'opérations spécifiques peuvent à leur tour vérifier la validité de toutes les opérations, vérifier les opérations de certains contextes uniquement ou convoquer l'utilisateur lorsqu'il existe des conditions inattendues dans une opération. Le fragment suivant montre comment une application peut configurer l'historique des opérations afin de mettre en oeuvre un modèle d'annulation linéaire pour toutes les opérations.
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());

Dans ce cas, un valideur d'opérations, LinearUndoEnforcer, fourni par l'environnement-cadre, est installé sur l'historique afin d'éviter l'annulation ou le rétablissement d'une opération qui n'est pas l'opération la plus récemment effectuée ou annulée dans tous ses contextes d'annulation.

Un autre valideur d'opération, LinearUndoViolationUserApprover, détecte la même condition et demande à l'utilisateur de préciser si l'opération doit être autorisée à poursuivre. Ce valideur d'opération peut être installé sur une partie particulière du plan de travail.

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);

Les développeurs de plug-ins sont libres de développer et d'installer leurs propres valideurs d'opérations pour mettre en oeuvre des modèles d'annulation spécifiques à l'application et des stratégies d'approbation.

L'opération Annuler et le plan de travail IDE

Nous avons vu les fragments de code utilisant le protocole du plan de travail pour accéder à l'historique des opérations et au contexte d'annulation du plan de travail. Ceci est possible grâce à IWorkbenchOperationSupport, qui peut être obtenu à partir du plan de travail. La notion de contexte d'annulation à l'échelle du plan de travail est assez générale. C'est à l'application du plan de travail de déterminer non seulement la portée spécifique impliquée par le contexte d'annulation du plan de travail mais également les vues et éditeurs utilisant le contexte du plan de travail lors de la prise en charge de l'annulation.

Dans le cas du plan de travail IDE d'Eclipse, le contexte d'annulation du plan de travail doit être assigné à toute opération affectant l'ensemble de l'espace de travail IDE. Ce contexte est utilisé par les vues qui utilisent l'espace de travail, notamment le navigateur de ressources. Le plan de travail IDE installe un adaptateur sur l'espace de travail de IUndoContext qui renvoie le contexte d'annulation du plan de travail. Cet enregistrement basé sur le modèle permet aux plug-ins utilisant l'espace de travail d'obtenir le contexte d'annulation approprié, même s'ils sont sans tête et ne font référence à aucune classe du plan de travail.

// 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
}

Il est souhaitable que les autres plug-ins utilisent cette même technique pour l'enregistrement des contextes d'annulation basés sur le modèle.