Operaciones susceptibles de deshacerse

Hemos visto muchas maneras distintas de contribuir con acciones al entorno de trabajo, pero no nos hemos centrado en la implementación del método run() de una acción. La mecánica del método depende de la acción específica de que se trate, pero la estructuración del código como una operación susceptible de deshacerse permite que la acción participe en el soporte de acciones de deshacer y rehacer de la plataforma.

La plataforma proporciona una infraestructura de operaciones susceptibles de deshacerse en el paquete org.eclipse.core.commands.operations. Al implementar el código que se encuentra dentro de un método run() para crear un IUndoableOperation, la operación puede estar disponible para deshacer y rehacer. Es sencillo convertir una acción para utilizar operaciones, aparte de la implementación misma del funcionamiento de deshacer y rehacer.

Cómo escribir una operación susceptible de deshacerse

Empezaremos examinando un ejemplo muy simple. Recuerde el ViewActionDelegate simple proporcionado en el conector de ejemplo de archivo readme. Cuando se invoca, la acción sólo lanza un diálogo que anuncia que se ha ejecutado.

public void run(org.eclipse.jface.action.IAction action) {
	MessageDialog.openInformation(view.getSite().getShell(),
		MessageUtil.getString("Readme_Editor"),  
		MessageUtil.getString("View_Action_executed")); 
}
Al utilizar operaciones, el método de ejecución es responsable de crear una operación que realiza el trabajo efectuado anteriormente en el método de ejecución y solicitar que un historial de operaciones ejecute la operación para que pueda recordarse para operaciones de deshacer y rehacer.
public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
La operación encapsula el antiguo comportamiento del método de ejecución, así como las acciones de deshacer y rehacer correspondientes a la operación.
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;
	}
}

Para las acciones simples, es posible mover todo el trabajo más detallado a la clase de operaciones. En este caso, puede que sea conveniente reducir las antiguas clases de acciones a una sola clase de acción que esté parametrizada. La acción simplemente ejecutará la operación proporcionada cuando sea el momento de ejecutarla. En gran medida, es una decisión basada en el diseño de la aplicación.

Cuando una acción lanza un asistente, la operación suele crearse como parte del método performFinish() del asistente o el método finish() de una página de asistente. Convertir el método finish para utilizar operaciones es similar a convertir un método run. El método es responsable de crear y ejecutar una operación que realice el trabajo efectuado anteriormente en línea.

Historial de operaciones

Hasta ahora hemos utilizado un historial de operaciones sin explicarlo realmente. Examinemos de nuevo el código que crea nuestra operación de ejemplo.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
¿Qué es el historial de operaciones? IOperationHistory define la interfaz para el objeto que hace un seguimiento de todas las operaciones susceptibles de deshacerse. Cuando un historial de operaciones ejecuta una operación, en primer lugar ejecuta la operación y luego lo añade al historial de acciones de deshacer. Los clientes que deseen deshacer y rehacer operaciones pueden hacerlo mediante el protocolo IOperationHistory.

El historial de operaciones utilizado por un aplicación puede recuperarse de diversas maneras. La manera más sencilla consiste en utilizar OperationHistoryFactory.

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

El entorno de trabajo puede utilizarse también para recuperar el historial de operaciones. El entorno de trabajo configura el historial de operaciones por omisión y también proporciona el protocolo para acceder al mismo. El fragmento siguiente muestra cómo obtener el historial de operaciones del entorno de trabajo.

IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
Una vez que se ha obtenido un historial de operaciones, puede utilizarse para consultar el historial de acciones de deshacer o rehacer, averigüe qué operación es la siguiente en línea para deshacer o rehacer, o bien deshaga o rehaga operaciones concretas. Los clientes pueden añadir un IOperationHistoryListener para recibir notificaciones sobre cambios en el historial. Otro protocolo permite a los clientes establecer límites en el historial o notificar cambios en una operación determinada a los escuchas. Antes de que examinemos el protocolo de manera detallada, es necesario entender el contexto de deshacer.

Contextos de deshacer

Cuando se crea una operación, se le asigna un contexto de deshacer que describe el contexto de usuario en que se realizó la operación original. Normalmente, el contexto de deshacer depende de la vista o del editor que originó la operación susceptible de deshacerse. Por ejemplo, los cambios efectuados dentro de un editor suelen ser locales en dicho editor. En este caso, el editor debe crear su propio contexto de operación de deshacer y asignar dicho contexto a las operaciones que añada al historial. De esta manera, todas las operaciones efectuadas en el editor se consideran locales y semiprivadas. Los editores o las vistas que operan en un modelo compartido suelen utilizar un contexto de deshacer que está relacionado con el modelo que manejan. Utilizando un contexto de deshacer más general, las operaciones realizadas por una vista o editor pueden estar disponibles para una acción de deshacer en otra vista o editor que opere en el mismo modelo.

Los contextos de deshacer tienen un comportamiento relativamente simple; el protocolo de IUndoContext es bastante reducido. El rol principal de un contexto consiste en "marcar" una operación concreta como perteneciente en ese contexto de deshacer, para distinguirla de las operaciones creadas en contextos de deshacer diferentes. Esto permite que el historial de operaciones haga un seguimiento del historial global de todas las operaciones susceptibles de deshacerse que se han ejecutado, mientras que las vistas y los editores pueden filtrar el historial de acuerdo a un punto de vista específico utilizando el contexto de deshacer.

Los contextos de deshacer pueden crearse con el conector que crea las operaciones susceptibles de deshacerse o se accede a las mismas mediante la API. Por ejemplo, el entorno de trabajo proporciona acceso a un contexto de deshacer que puede utilizar para las operaciones para todo el entorno de trabajo. Independientemente de cómo se hayan obtenido, al crear una operación deben asignarse contextos de deshacer. El fragmento siguiente muestra cómo el ViewActionDelegate del conector de archivo readme podría asignar un contexto de todo el entorno de trabajo a sus operaciones.

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

¿Por qué utilizar contextos de deshacer? ¿Por qué no utilizar historiales de operaciones distintos para vistas y editores diferentes? Si se utilizan historiales de operaciones distintos, se da por supuesto que cualquier vista o editor concreto mantiene su historial privado de acciones de deshacer y que la acción de deshacer no tiene significado global en la aplicación. Esto puede ser adecuado para algunas aplicaciones y, en estos casos, cada vista o editor debe crear su propio contexto de deshacer distinto. Otras aplicaciones pueden querer implementar una acción de deshacer global que se aplique a todas las operaciones de usuario, independientemente de la vista o el editor en que se originen. En este caso, todos los conectores que añadan operaciones en el historial debe utilizar el contexto del entorno de trabajo.

En las aplicaciones más complicadas, la acción de deshacer no es estrictamente local ni global. En su lugar, existe un entrecruzamiento entre contextos de deshacer. Esto puede conseguirse asignando varios contextos a una operación. Por ejemplo, una vista de entorno de trabajo IDE puede manejar todo el área de trabajo y considerar que el área de trabajo es su contexto de deshacer. Un editor abierto en un recurso concreto en el área de trabajo puede considerar que sus operaciones son principalmente locales. Sin embargo, las operaciones realizadas en el editor pueden afectar tanto al recurso concreto como al área de trabajo en general. (Un buen ejemplo de este caso es el soporte de refactorización de JDT, lo que permite que se produzcan cambios estructurales en un elemento Java mientras se edita el archivo de origen). En estos casos, es útil poder añadir ambos contextos de deshacer a la operación para que la acción de deshacer pueda realizarse desde el propio editor, así como aquellas vistas que manejan el área de trabajo.

Ahora que entendemos lo que realiza un contexto de deshacer, podemos volver a examinar el protocolo para IOperationHistory. El siguiente fragmento se utiliza para realizar una acción de deshacer en un contexto determinado:

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
try {
	IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// manejar la excepción 
}
El historial obtendrá la operación realizada más recientemente que tiene el contexto dado y le pedirá que se deshaga. Puede utilizarse otro protocolo para obtener todo el historial de acciones de deshacer o rehacer relativo a un contexto o encontrar la operación que se deshará o rehará en un contexto determinado. El fragmento siguiente obtiene la etiqueta para la operación que se deshará en un contexto concreto.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(myContext).getLabel();

Puede utilizarse el contexto global de deshacer, IOperationHistory.GLOBAL_UNDO_CONTEXT, para hacer referencia al historial global de deshacer. Es decir, a todas las operaciones del historial independientemente de su contexto específico. El siguiente fragmento obtiene el historial global de deshacer.

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

Siempre que se ejecuta, deshace o rehace una operación utilizando el protocolo de historial de operaciones, los clientes pueden proporcionar un supervisor de progreso e información de UI adicional que pueden ser necesarios para realizar la operación. Esta información se pasa a la propia operación. En nuestro ejemplo original, la acción de readme construía una operación con un parámetro de shell que podría utilizarse para abrir el diálogo. En vez de almacenar el shell en la operación, es un enfoque mejor el pasar parámetros a los métodos de ejecutar, deshacer y rehacer que proporcionan información de UI necesaria para ejecutar la operación. Estos parámetros se pasarán a la propia operación.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation();
	...
	operationHistory.execute(operation, null, infoAdapter);
}
El infoAdapter es un IAdaptable que puede proporcionar mínimamente el Shell que puede utilizarse al lanzar diálogos. En nuestra operación de ejemplo se utilizaría este parámetro de la manera siguiente:
	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;
		}
	}
		// hacer algo más...
}

Manejadores de acciones de deshacer y rehacer

La plataforma proporcionar manejadores de acciones redirigibles de deshacer y rehacer estándar que las vistas y los editores pueden configurar para proporcionar soporte de deshacer y rehacer para su contexto concreto. Cuando se crea el manejador de acciones, se le asigna un contexto para que se filtre el historial de operaciones de una manera adecuada para esa vista en particular. Los manejadores de acciones se encargan de actualizar las etiquetas de deshacer y rehacer para mostrar la operación actual en cuestión, proporcionar el supervisor de progreso y la información de UI adecuados al historial de operaciones y, opcionalmente, podar el historial cuando la operación actual no sea válida. Por razones prácticas se proporciona un grupo de acciones que crea los manejadores de acciones y los asigna a las acciones globales de deshacer y rehacer.

new UndoRedoActionGroup(this.getSite(), undoContext, true);
El último parámetro es un valor booleano que indica si los historiales de deshacer y rehacer para el contexto especificado deben desecharse cuando la operación disponible actualmente para deshacer o rehacer no sea válida. El valor de este parámetro está relacionado con el contexto de deshacer proporcionado y la estrategia de validación utilizada por las operaciones con ese contexto.

Modelos de deshacer de aplicación

Anteriormente hemos examinado cómo pueden utilizarse los contextos de deshacer para implementar diferentes clases de modelos de deshacer de aplicación. La capacidad de asignar uno o más contextos a operaciones permite que las aplicaciones implementen estrategias de deshacer que son estrictamente locales a cada vista o editor, estrictamente globales en todos los conectores, o algún modelo intermedio. Otra decisión de diseño que implica acciones de deshacer y rehacer consiste en si cualquier operación se puede deshacer o rehacer en cualquier momento, o si el modelo es estrictamente lineal, en que sólo se considera la operación más reciente para acciones de deshacer o rehacer.

IOperationHistory define un protocolo que permite modelos de deshacer flexibles, que deja a las implementaciones individuales determinar qué es lo que está permitido. En el protocolo de deshacer y rehacer que hemos visto hasta ahora se da por supuesto que sólo hay una operación implicada disponible para realizar acciones de deshacer o rehacer en un contexto de deshacer concreto. Se proporciona un protocolo adicional para permitir a los clientes que ejecuten una operación específica, independientemente de su posición en el historial. El historial de operaciones puede configurarse de manera que pueda implementarse el modelo adecuado para una aplicación. Esto se realiza con una interfaz que se utiliza para preaprobar cualquier petición de deshacer o rehacer antes de que se deshaga o rehaga la operación.

Aprobadores de operaciones

IOperationApprover define el protocolo para aprobar acciones de deshacer y rehacer de una operación determinada. Un aprobador de operaciones se ha instalado en un historial de operaciones. Aprobadores de operaciones específicos pueden, a su vez, comprobar la validez de todas las operaciones, comprobar las operaciones de ciertos contextos únicamente, o enviar una solicitud al usuario cuando se encuentren condiciones inesperadas en una operación. El fragmento siguiente muestra cómo una aplicación puede configurar el historial de operaciones para forzar un modelo de deshacer lineal para todas las operaciones.
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// establecer un aprobador en el historial que no permitirá ninguna acción de deshacer que no sea la operación más reciente
history.addOperationApprover(new LinearUndoEnforcer());

En este caso, un aprobador de operaciones proporcionado por la infraestructura, LinearUndoEnforcer, se instala en el historial para impedir una acción de deshacer o rehacer en cualquier operación que no sea la que se ha realizado o deshecho más recientemente en todos sus contextos de deshacer.

Otro aprobador de operaciones, LinearUndoViolationUserApprover, detecta la misma condición y solicita al usuario si debe permitirse que continúe la operación. Este aprobador de operaciones puede instalarse en un componente determinado del entorno de trabajo.

IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// establecer un aprobador en este componente que enviará una solicitud al usuario cuando la operación no sea la más reciente
IOperationApprover approver = new LinearUndoViolationUserApprover(myUndoContext, myWorkbenchPart);
history.addOperationApprover(approver);

Los desarrolladores de conectores pueden desarrollar e instalar sus propios aprobadores de operaciones para implementar modelos de deshacer y estrategias de aprobación específicas de la aplicación.

Acciones de deshacer y el entorno de trabajo IDE

Hemos visto fragmentos de código que utilizan el protocolo de entorno de trabajo para acceder al historial de operaciones y el contexto de deshacer del entorno de trabajo. Esto se consigue utilizando IWorkbenchOperationSupport, que puede obtenerse del entorno de trabajo. La noción de un contexto de deshacer de todo el entorno de trabajo es bastante general. Corresponde a la aplicación del entorno de trabajo el determinar qué ámbito específico se implica por el contexto de deshacer del entorno de trabajo y qué vistas o editores utilizan el contexto de entorno de trabajo al proporcionar el soporte de la acción de deshacer.

En el caso del entorno de trabajo Eclipse IDE, el contexto de deshacer de entorno de trabajo debe asignarse a cualquier operación que afecte al entorno de trabajo IDE en general. Este contexto se utiliza por vistas que manejan el área de trabajo como, por ejemplo, Resource Navigator. El entorno de trabajo IDE instala un adaptador en el área de trabajo para IUndoContext que devuelve el contexto de deshacer de entorno de trabajo. Este registro basado en un modelo permite conectores que manejen el área de trabajo para obtener el contexto de deshacer adecuado, aunque no tengan cabecera ni hagan referencia a ninguna clase de entorno de trabajo.

// obtener el historial de operaciones
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// obtener el contexto de deshacer adecuado para mi modelo
IUndoContext workspaceContext = (IUndoContext)ResourcesPlugin.getWorkspace().getAdapter(IUndoContext.class);
if (workspaceContext != null) {
	// crear una operación y asignarle el contexto
}

Se anima a que otros conectores utilicen esta misma técnica para registrar contextos de deshacer basados en modelos.