Operações que Podem Ser Desfeitas

Examinamos várias maneiras diferentes de contribuir com ações para o workbench, mas não focalizamos na implementação de um método de ação run(). A mecânica do método depende da ação específica em questão, mas a estruturação do código como uma operação que pode ser desfeita permite que a ação participe do suporte aos comandos desfazer e refazer da plataforma.

A plataforma fornece uma estrutura de operações que podem ser desfeitas no pacote org.eclipse.core.commands.operations. Ao implementar o código dentro de um método run() para criar uma IUndoableOperation, a operação pode se tornar disponível para os comandos desfazer e refazer. A conversão de uma ação para utilizar operações é direta, além de implementar o próprio comportamento dos comandos desfazer e refazer.

Gravando uma Operação que Pode Ser Desfeita

Iniciaremos examinando um exemplo muito simples. Lembre-se do ViewActionDelegate simples fornecido no plug-in de exemplo do leia-me. Quando chamada, a ação simplesmente ativa um diálogo que anuncia que ela foi executada.

public void run(org.eclipse.jface.action.IAction action) {
	MessageDialog.openInformation(view.getSite().getShell(),
		MessageUtil.getString("Readme_Editor"),
		MessageUtil.getString("View_Action_executed"));
}
Utilizando operações, o método run é responsável por criar uma operação que faz o trabalho anteriormente feito no método run e solicitando que um histórico de operações execute a operação, para que ele possa ser lembrado de desfazer e refazer.
public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell());
	...
	operationHistory.execute(operation, null, null);
}
A operação encapsula o comportamento antigo do método run, bem como os comandos desfazer e refazer da operação.
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 ações simples, pode ser possível mover todos os elementos básicos que funcionam na classe de operações. Nesse caso, pode ser apropriado reduzir as classes de ações anteriores em uma única classe de ações com parâmetros. A ação executaria apenas a operação fornecida, no momento da execução. Essa é basicamente uma decisão de design do aplicativo.

Quando uma ação ativa um assistente, a operação é tipicamente criada como parte do método performFinish() do assistente ou de um método finish() da página do assistente. A conversão do método finish para utilizar operações é semelhante a converter um método run. O método é responsável por criar e executar uma operação que faz o trabalho anteriormente feito de modo seqüencial.

Histórico de Operações

Até agora, utilizamos um histórico de operações sem realmente explicá-lo. Examinemos novamente o código que cria nossa operação de exemplo.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell());
	...
	operationHistory.execute(operation, null, null);
}
Sobre o que é o histórico de operações? O IOperationHistory define a interface do objeto que controla todas as operações que podem ser desfeitas. Quando um histórico de operações executa uma operação, ele primeiro executa a operação e, em seguida, a inclui no histórico do comando desfazer. Os clientes que desejam desfazer e refazer operações o fazem utilizando o protocolo IOperationHistory.

O histórico de operações utilizado por um aplicativo pode ser recuperado de várias maneiras. A maneira mais simples de utilizar o OperationHistoryFactory.

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

O workbench também pode ser utilizado para recuperar o histórico de operações. O workbench configura o histórico de operações padrão e também fornece o protocolo para acessá-lo. O fragmento a seguir demonstra como obter o histórico de operações do workbench.

IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
Quando um histórico de operações é obtido, ele pode ser utilizado para consultar o histórico dos comandos desfazer e refazer, descobrir qual é a próxima operação na fila a ser desfeita ou refeita ou para desfazer ou refazer operações específicas. Os clientes podem incluir um IOperationHistoryListener para receber notificações sobre alterações no histórico. Outro protocolo permite que os clientes configurem limites no histórico ou notifiquem listeners sobre alterações em uma operação específica. Antes de examinarmos o protocolo em detalhes, precisamos entender o contexto do comando desfazer.

Contextos do Comando Desfazer

Quando uma operação é criada, um contexto do comando desfazer é designado a ela, descrevendo o contexto do usuário no qual a operação original foi executada. Tipicamente, o contexto do comando desfazer depende da visualização ou do editor que originou a operação que pode ser desfeita. Por exemplo, as alterações feitas dentro de um editor são muitas vezes locais para esse editor. Nesse caso, o editor deve criar seu próprio contexto de comando desfazer e designar esse contexto para operações que ele inclui no histórico. Dessa maneira, todas as operações executadas no editor são consideradas locais e semi-privadas. Os editores e as visualizações que operam em um modelo compartilhado sempre utilizam um contexto de comando desfazer que é relatado para o modelo que eles estão manipulando. Ao utilizar um contexto de comando desfazer mais geral, as operações desempenhadas por uma visualização ou editor podem estar disponíveis para o comando desfazer em outra visualização ou editor que opera no mesmo modelo.

Contextos de desfazer são relativamente simples em relação a comportamento. O protocolo do IUndoContext é razoavelmente mínimo. A função principal de um contexto é "marcar" uma operação específica como pertencente àquele contexto de comando desfazer, para distingui-la de operações criadas em contextos diferentes de desfazer. Isso permite que o histórico de operações mantenha um controle do histórico global de todas as operações que podiam ser desfeitas que foram executadas, enquanto as visualizações e editores podem filtrar o histórico para um ponto específico da visualização utilizando o contexto de desfazer.

Contextos de desfazer podem ser criados pelo plug-in que está criando as operações que podem ser desfeitas ou acessadas pela API. Por exemplo, o workbench fornece acesso a um contexto de desfazer que pode ser utilizado por operações de todo o workbench. Embora eles sejam obtidos, os contextos de desfazer devem ser designados quando uma operação é criada. O fragmento a seguir mostra como o ViewActionDelegate do plug-in do leia-me pode designar um contexto em nível de workbench para suas operações.

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 que utilizar contextos de desfazer afinal? Por que não utilizar históricos separados de operações para visualizações e editores separados? A utilização de históricos de operações separados assume que qualquer visualização ou editor específico mantém seu próprio histórico privado do comando desfazer e que a função desfazer não tem significado global no aplicativo. Isso pode ser apropriado para alguns aplicativos e nesses casos cada visualização ou editor deve criar seu próprio contexto separado de desfazer. Outros aplicativos podem desejar implementar uma função desfazer global que é aplicada a todas as operações do usuário, independentemente da visualização ou do editor onde foram originadas. Nesse caso, o contexto do workbench deve ser utilizado por todos os plug-ins que incluem operações no histórico.

Em aplicativos mais complicados, a função desfazer não é estritamente local nem estritamente global. Em vez disso, existem algumas alternativas entre contextos de desfazer. Isso pode ser conseguido, designando vários contextos a uma operação. Por exemplo, uma visualização de workbench IDE pode manipular todo o workbench e considerar o espaço de trabalho como seu contexto de desfazer. Um editor aberto em um recurso específico no espaço de trabalho pode considerar suas operações como predominantemente locais. No entanto, operações executadas dentro do editor podem na verdade afetar o recurso específico e o espaço de trabalho de forma geral. Um bom exemplo desse caso é o suporte à recriação do JDT que permite que alterações estruturais em um elemento Java ocorram enquanto o arquivo de origem é editado. Nesses casos, é útil poder incluir os dois contextos de desfazer na operação para que o comando desfazer possa ser executado no próprio editor, bem como naquelas visualizações que manipulam o espaço de trabalho.

Agora que compreendemos o que um contexto de desfazer faz, podemos examinar novamente o protocolo para IOperationHistory. O fragmento a seguir é utilizado para executar um comando desfazer em algum contexto:

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
try {
	IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// manipular a exceção
}
O histórico obterá a última operação executada que possui o contexto fornecido e pedirá a ela que se desfaça. Outro protocolo pode ser utilizado para obter todo o histórico dos comandos desfazer e refazer de um contexto ou localizar a operação que será desfeita ou refeita em um contexto específico. O fragmento a seguir obtém a etiqueta da operação que será desfeita em um contexto específico.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(myContext).getLabel();

O contexto de comando desfazer global, IOperationHistory.GLOBAL_UNDO_CONTEXT, pode ser utilizado para se referir ao histórico de comando desfazer global. Isto é, para todas as operações no histórico independente de seu contexto específico. O fragmento a seguir obtém o histórico de comando desfazer global.

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

Sempre que uma operação é executada, desfeita ou refeita utilizando o protocolo de histórico de operações, os clientes podem fornecer um monitor de progresso e informações adicionais da UI que podem ser necessárias para desempenhar a operação. Essas informações são transmitidas para a própria operação. Em nosso exemplo original, a ação leia-me construiu uma operação com um parâmetro shell que pôde ser utilizado para abrir o diálogo. Em vez de armazenar o shell na operação, uma abordagem melhor é transmitir parâmetros para os métodos execute, undo e redo que fornecem todas as informações da UI necessárias para executar a operação. Esses parâmetros serão transmitidos para a própria operação.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation();
	...
	operationHistory.execute(operation, null, infoAdapter);
}
O infoAdapter é um IAdaptable que pode fornecer de forma mínima o Shell que pode ser utilizado ao ativar diálogos. Nossa operação de exemplo utiliza esse parâmetro da seguinte maneira:
	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;
		}
	}
		// fazer algo mais...
}

Rotinas de Tratamento de Ação dos Comandos Desfazer e Refazer

A plataforma fornece rotinas de tratamento de ação que pode ser executada novamente que podem ser configuradas por visualizações e editores para fornecer suporte aos comandos desfazer e refazer para seu contexto específico. Quando a rotina de tratamento da ação é criada, um contexto é designado a ela para que o histórico de operações seja filtrado de maneira adequada para aquela visualização específica. As rotinas de tratamento de ação tomam conta da atualização das etiquetas dos comandos desfazer e refazer para mostrar a operação atual em questão, fornecendo o monitor de progresso apropriado e informações da UI para o histórico de operações e, opcionalmente, recortando o histórico quando a operação atual é inválida. Um grupo de ações que cria as rotinas de tratamento de ações e as designa às ações de desfazer e refazer globais é fornecido para conveniência.

new UndoRedoActionGroup(this.getSite(), undoContext, true);
O último parâmetro é um booleano que indica se os históricos dos comandos desfazer e refazer do contexto especificado deve ser descartado quando a operação disponível no momento para o comando desfazer ou refazer não é válida. A configuração desse parâmetro é relacionada ao contexto de desfazer fornecido e à estratégia de validação utilizada por operações com aquele contexto.

Modelos de Comando Desfazer de Aplicativos

Anteriormente, examinamos como contextos de desfazer podem ser utilizados para implementar diferentes tipos de modelos de comando desfazer do aplicativo. A habilidade de designar um ou mais contextos a operações permite que aplicativos implementem estratégias de comando desfazer que são estritamente locais para cada visualização ou editor, estritamente globais entre todos os plug-in ou algum modelo no meio. Outra decisão de design envolvendo os comandos desfazer e refazer é se qualquer operação pode ser desfeita ou refeita a qualquer momento ou se o modelo é estritamente linear, com apenas a operação mais recente sendo considerada para desfazer ou refazer.

IOperationHistory define o protocolo que permite modelos flexíveis do comando desfazer, deixando que implementações individuais determinem o que é permitido. O protocolo dos comandos desfazer e refazer que vimos até agora supõe que existe apenas uma operação implícita disponível para comandos desfazer e refazer em um determinado contexto de desfazer. Protocolo adicional é fornecido para permitir que clientes executem uma operação específica, independentemente de sua posição no histórico. O histórico de operações pode ser configurado de forma que o modelo apropriado de um aplicativo possa ser implementado. Isso é feito com uma interface que é utilizada para pré-aprovar qualquer pedido de comando desfazer ou refazer antes que a operação seja desfeita ou refeita.

Aprovadores da Operação

IOperationApprover define o protocolo para aprovar comandos desfazer e refazer de uma operação específica. Um aprovador de operação é instalado em um histórico de operações. Os aprovadores de operações específicos podem por sua vez, verificar a validade de todas as operações, verificar as operações de apenas determinados contextos ou solicitar ao usuário quando condições inesperadas são encontradas em uma operação. O fragmento a seguir mostra como um aplicativo pode configurar o histórico de operações para impor um modelo linear de comando desfazer para todas as operações.
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// configurar um aprovador no histórico que reprovará qualquer
comando desfazer que não seja a operação mais recente
history.addOperationApprover(new LinearUndoEnforcer());

Nesse caso, um aprovador de operação fornecido pela estrutura, LinearUndoEnforcer, é instalado no histórico para evitar o comando desfazer e refazer de qualquer operação que não seja a operação desfeita ou refeita mais recentemente em todos os contextos de comando desfazer.

Outro aprovador de operação, LinearUndoViolationUserApprover, detecta a mesma condição e solicita o usuário quanto a permissão de continuidade da operação. Esse aprovador de operação pode ser instalado em uma parte específica do workbench.

IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// configurar um aprovador nesta parte que solicitará a usuário
quando a operação não for a mais recente.
IOperationApprover approver = new
LinearUndoViolationUserApprover(myUndoContext, myWorkbenchPart);
history.addOperationApprover(approver);

s desenvolvedores de plug-ins são livres para desenvolver e instalar seus próprios aprovadores de operações, para implementação de modelos de comando desfazer específico do aplicativo e estratégias de aprovação.

Comando Desfazer e o Workbench IDE

Vimos trechos de código que utilizam protocolo do workbench para acessar o histórico de operações e o contexto de desfazer do workbench. Isso é realizado utilizando IWorkbenchOperationSupport, que pode ser obtido do workbench. A noção de um contexto de desfazer em nível de workbench é razoavelmente geral. É de responsabilidade do aplicativo do workbench determinar qual escopo específico está implícito no contexto de desfazer do workbench e quais visualizações ou editores utilizam o contexto do workbench ao fornecer suporte ao comando desfazer.

No caso do workbench do Eclipse IDE, o contexto de desfazer do workbench deve ser designado a qualquer operação que afete o espaço de trabalho do IDE em geral. Esse contexto é utilizado por visualizações que manipulam o espaço de trabalho, como o Navegador de Recurso. O workbench IDE instala um adaptador no espaço de trabalho para o IUndoContext que retorna o contexto de desfazer do workbench. Esse registro com base em modelo permite que plug-ins que manipulam o espaço de trabalho obtenham o contexto de desfazer apropriado, mesmo que ele seja headless e não faça referência a nenhuma classe de workbench.

// obter o histórico de operações
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// obter o contexto de desfazer apropriado para meu modelo
IUndoContext workspaceContext = (IUndoContext)ResourcesPlugin.getWorkspace().getAdapter(IUndoContext.class);
if (workspaceContext != null) {
	// criar uma operação e designar o contexto a ela
}

Outros plug-ins são incentivados a utilizar essa mesma técnica para registrar contextos de desfazer com base em modelo.