Operações não anuláveis

Já vimos muitas maneiras diferentes de contribuir com acções para a área de trabalho, mas não focámos a a implementação do método run() de uma acção. A mecânica do método depende da acção específica em questão, mas se estruturarmos o código como operação não anulável permitimos à acção participar no suporte de anular e repetir da plataforma.

A plataforma faculta um quadro de operações não anuláveis 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 ser disponibilizada para anular e repetir. É simples converter uma acção para utilizar operações, salvo a implementação do comportamento de anular e repetir propriamente dito.

Compor uma operação não anulável

Começamos por observar um exemplo muito simples. Recordemos o simples ViewActionDelegate facultado no exemplo do plug-in readme. Quando invocada, a acção lança simplesmente uma caixa de diálogo que anuncia que 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"));
}
Ao utilizar operações, o método run é responsável por criar uma operação que realize o trabalho anteriormente feito no método run e por pedir que um histórico de operações execute a operação, de modo a ser recordada para anular e repetir.
public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
A operação engloba o antigo comportamento do método run, bem como anular e repetir para a operação.
class ReadmeOperation extends AbstractOperation {
	Shell shell;
	public ReadmeOperation(Shell shell) {
		super("Operação Readme");
		this.shell = shell;
	}
	public IStatus execute(IProgressMonitor monitor, IAdaptable info) {
		MessageDialog.openInformation(shell,
		MessageUtil.getString("Editor_Readme"),
		MessageUtil.getString("Acção_Vista_executada"));
		return Status.OK_STATUS;
	}
	public IStatus undo(IProgressMonitor monitor, IAdaptable info) {
		MessageDialog.openInformation(shell,
		MessageUtil.getString("Editor_Readme"),
			"Undoing view action");
		return Status.OK_STATUS;
	}
	public IStatus redo(IProgressMonitor monitor, IAdaptable info) {
		MessageDialog.openInformation(shell,
		MessageUtil.getString("Editor_Readme"),
			"Redoing view action");
		return Status.OK_STATUS;
	}
}

No caso de acções simples, poderá ser viável mover todo o trabalho de sapa para a classe da operação. Neste caso, poderá ser apropriado contrair as anteriores classes de acção numa única classe de acção que seja parametrizada. A acção iria simplesmente executar a operação facultada quando fosse altura de a executar. Isto é maioritariamente uma decisão inerente à concepção da aplicação.

Quando uma acção lança um assistente, a operação é geralmente criada como parte do método performFinish() do assistente ou de um método finish() de página de assistente. Converter o método finish para utilizar operações é semelhante a converter o método run. O método é responsável por criar e executar uma operação que realize o trabalho anteriormente feito em linha.

Histórico de operações

Até agora utilizámos um histórico de operações sem o explicar. Vejamos novamente o código que cria a nossa operação exemplo.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation(
		view.getSite().getShell()); 
	...
	operationHistory.execute(operation, null, null);
}
De que trata o histórico de operações? O IOperationHistory define a interface para o objecto que acompanha todas as operações não anuláveis. Quando um histórico de operações executa uma operação, primeiro executa-a e depois adiciona-a ao histórico de anular. Os clientes que quiserem anular e repetir operações fazem-no com o protocolo IOperationHistory.

O histórico de operações utilizado por uma aplicação pode ser obtido de várias formas. A mais simples consiste em utilizar a OperationHistoryFactory.

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

A área de trabalho também pode ser usada para obter o histórico de operações. A área de trabalho configura o histórico de operações e faculta também o protocolo para aceder a ele. A porção de código seguinte mostra como obter o histórico de operações da área de trabalho.

IWorkbench workbench = view.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
Uma vez obtido o histórico de operações, este pode ser usado para consultar o histórico de anulações ou repetições, saber qual a próxima operação a anular ou repetir ou para anular ou repetir determinadas operações. Os clientes podem adicionar um IOperationHistoryListener para receberem notificações sobre alterações ao histórico. Os outros protocolos permitem aos clientes definirem limites para o histórico ou notificarem ouvintes sobre alterações a determinada operação. Anets de vermos o protocolo em detalhe, temos de compreender o contexto de anulação.

Contextos de anulação

Quando se cria uma operação, esta recebe um contexto de anulação que descreve o contexto de utilizador no qual foi executada a operação original. O contexto de anulação geralmente depende da vista ou do editor que originou a operação não anulável. Por exemplo, as alterações feitas dentro de um editor costumam ser locais a esse editor. Neste caso, o editor deve criar o seu próprio contexto de anulação e atribuí-lo a operações que adicione ao histórico. Desta forma, todas as operações executadas no editor são consideradas locais e semi-privadas. Os editores ou as vistas que funcionam num modelo partilhado geralmente usam um contexto de anulação que está relacionado com o modelo que estiverem a manipular. Ao usar um contexto de anulação mais geral, as operações realizadas por uma vista ou um editor poderão ficar disponíveis para anular noutra vista ou editor que funcione no mesmo modelo.

Os contextos de anulação são relativamente simples de comportamento; o protocolo para IUndoContext é mínimo. O principal papel de um contexto consiste em "identificar" determinada operação como pertencente a esse contexto de anulação, a fim de a distinguir de operações criadas em contextos de anulação diferentes. Isto permite ao histórico de operações acompanhar o histórico global de todas as operações não anuláveis que foram executadas, enquanto vistas e editores podem filtrar o histórico para um ponto de vista específico através do contexto de anulação.

Os contextos de anulação podem ser criados pelo plug-in que estiver a criar as operações não anuláveis ou acedidos através de APIs. Por exemplo, a área de trabalho faculta acesso a um contexto de anulação que pode ser usado em operações em toda a área de trabalho. Seja como for que são obtidos, os contexto de anulação devem ser atribuídos quando se cria uma operação. A porção de código seguinte mostra como o ViewActionDelegate do plug-in readme poderia atribuir um contexto abrangente de área de trabalho às 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);
}

Posto isto, para quê usar contextos de anulação? Por que não utilizar históricos de operações separados em vistas e editores separados? A utilização de históricos de operações parte do princípio de que dada vista ou dado editor mantém o seu próprio histórico de anulações privado e que essas anulações não têm significado global na aplicação. Tal poderá ser apropriado para algumas aplicações e nesses casos cada vista ou editor deve criar o seu próprio contexto de anulação separado. As outras aplicações podem optar por implementar uma anulação global que se aplique a todas as operações de utilizador, seja qual for a vista ou o editor onde tenham origem. Neste caso, o contexto da área de trabalho deve ser usado por todos os plug-ins que adicionem operações ao histórico.

Em aplicações mais complicadas, a anulação nem é estritamente local nem estritamente global. Em contrapartida, existe algum cruzamento entre contextos de anulação. Tal pode ser realizado atribuindo vários contextos a uma operação. Por exemplo, uma vista de área de trabalho IDE poderá manipular todo o espaço de trabalho e considerá-lo o seu contexto de anulação. Um editor que esteja aberto em determinado recurso no espaço de trabalho poderá considerar as suas operações maioritariamente locais. Todavia, as operações executadas dentro do editor poderão realmente afectar tanto o recurso em particular como o espaço de trabalho em geral. (Um bom exemplo deste caso é o suporte de refactorização das JDT, o qual permite que ocorram alterações estruturais a um elemento Java enquanto se edita o ficheiro origem). Nestes casos, é útil poder adicionar tanto contextos de anulação à operação para que a anulação possa ser realizada a partir do próprio editor, como as vistas que manipulam o espaço de trabalho.

Agora que compreendemos o que faz um contexto de anulação, podemos ver novamente o protocolo para IOperationHistory. A porção de código seguinte é utilizada para realizar uma anulação em algum contexto:

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
  try {
	IStatus status = operationHistory.undo(oMeuContexto, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// handle the exception
}
O histórico irá obter a operação realizada mais recentemente que tenha o contexto e pedir-lhe que se anule a si própria. Podem ser usados ourtos protocolos para obter o histórico de anulações ou repetições inteiro de um contexto ou para localizar a operação que será anulada ou repetida em determinado contexto. A porção de código seguinte obtém a etiqueta para a operação que será anulada em determinado contexto.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(oMeuContexto).getLabel();

O contexto de anulação global, IOperationHistory.GLOBAL_UNDO_CONTEXT, poderá ser usado a fim de remeter para o histórico de anulações global. Significa isto remeter para todas as operações no histórico independentemente do seu contexto específico. A porção de código seguinte obtém o histórico de anulações global:

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

Sempre que uma operação é executada, anulada ou repetida com o protocolo de histórico de operações, os clientes podem facultar um supervisor de progresso e outras informações de UI que possam ser necessárias para realizar a operação. Estas informações são transmitidas à própria operação. No nosso exemplo original, a acção readme construía uma operação com um parâmetro shell que podia ser usado para abrir a caixa de diálogo. Em vez de armazenar a shell na operação, a melhor abordagem consiste em transmitir parâmetros aos métodos execute, undo e redo que facultem informações de UI necessárias para executar a operação. Estes parâmetros serão transmitidos à 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 facultar minimamente a Shell que se pode usar ao lançar caixas de diálogo. A nossa operação exemplo usaria este parâmetro desta forma:
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("Editor_Readme"),
		MessageUtil.getString("Acção_Vista_executada"));
			return Status.OK_STATUS;
		}
	}
	// do something else...
}

Rotinas de tratamento de acções de anulação e repetição

A plataforma proporciona rotinas de tratamento de acções redestináveis de anulação e repetição normalizadas que podem ser configuradas por vistas e editores a fim de facultar suporte de anulação e repetição para determinado contexto. Quando se cria a rotina de tratamento de acções, é-lhe atribuído um contexto para que o histórico de operações seja filtrado de maneira apropriada a essa vista. As rotinas de tratamento de acções encarregam-se de actualizar as etiquetas de anulação e repetição a fim de mostrar a actual operação em análise, de facultar o supervisor de progresso e as informações de UI apropriadas ao histórico de operações e de desmarcar opcionalmente o histórico quando a operação actual não for válida. É facultado um grupo de acções que cria as rotinas de tratamento de acções e as atribui às acções de anulação e repetição globais, por conveniência.

new UndoRedoActionGroup(this.getSite(), undoContext, true);
O último parâmetro é um valor booleano que indica se devem ser inutilizados ou não os históricos de anulações e repetições do contexto especificado, quando a operação actualmente disponível para anular ou repetir não for válida. A definição deste parâmetro está relacionada com o contexto de anulação facultado e a estratégia de validação utilizada por operaçoes com esse contexto.

Modelos de anulação de aplicações

Anteriormente vimos como os contextos de anulação podem ser usados para implementar diferentes tipos de modelos de anulação de aplicações. A capacidade de atribuir um ou mais contextos a operações permite às aplicações implementarem estratégias de anulação que sejam estritamente locais a cada vista ou editor, estritamente globais a todos os plug-ins ou algum modelo intermédio. Outra decisão de concepção inerente a anular e repetir é a possibilidade de anular ou repetir uma operação em qualquer altura ou se o modelo é estritamente linear, somente com a operação mais recente a ser considerada para anular ou repetir.

O IOperationHistory define protocolo que permite modelos de anulação flexíveis, deixando às implementações individuais a determinação do que é permitido. O protocolo de anulação e repetição que vimos até agora pressupõe que só haja uma operação implícita para anular ou repetir em determinado contexto de anulação. É facultado protocolo adicional para permitir aos clientes executar uma operação específica, seja qual for a sua posição no histórico. O histórico de operações pode ser configurado para que seja implementado o modelo apropriado a uma aplicação. Tal realiza-se com uma interface que é usada para aprovar previamente qualquer pedido de anulação ou repetição antes de a operação ser anulada ou repetida.

Aprovadores de operações

Um IOperationApprover define o protocol para aprovar anulações ou repetições de determinada operação. Um aprovador de operações é instalado num histórico de operações. Os aprovadores de operações específicos podem depois verificar a validade de todas as operações, verificar operações de certos contextos somente ou perguntar ao utilizador quando depararem com condições inesperadas numa operação. A porção de código seguinte mostra como uma aplicação poderia configurar o histórico de operações de modo a aplicar um modelo de anulações linear a todas as operações.
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());

Neste caso, um aprovador de operações facultado pelo quadro, LinearUndoEnforcer, é instalado no histórico para impedir a anulação ou repetição de qualquer operação que não seja a mais recente, anulada ou repetida, e todos os seus contextos de anulação.

Outro aprovador de operações, LinearUndoViolationUserApprover, detecta a mesma condição e pergunta ao utilizador se a operação deve ser autorizada a continuar ou não. Este aprovador de operações pode ser instalado em determinada parte da área de trabalho.

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

Os programadores de plug-ins são livres de desenvolver e instalar os seus próprios aprovadores de operações para implementar modelos de anulação e estatégias de aprovação específicas de aplicações. No plug-in, talvez seja apropriado obter aprovação para a execução original de uma operação, para além de anular e refazer a operação. Se for o caso, o aprovador de operação deverá também implementar o IOperationApprover2, que aprova a execução da operação. Quando for pedida a execução de uma operação, o histórico de operações da plataforma tentará obter aprovação de qualquer aprovador de operações que implemente esta interface.

Anulação e a Área de trabalho IDE

Já vimos porções de código que utilizam protocolo de área de trabalho para aceder ao histórico de operações e ao contexto de anulações da área de trabalho. Tal realiza-se com IWorkbenchOperationSupport, o qual se pode obter junto da área de trabalho. A noção de um contexto de anulações englobante de toda a área de trabalho está generalizado. Compete à aplicação de área de trabalho determinar qual o âmbito específico implícito pelo contexto de anulações de área de trabalho e quais as vistas ou editores que utilizam o contexto de área de trabalho ao facultarem suporte de anulação.

No caso da área de trabalho IDE do Eclipse, o contexto de anulações da área de trabalho deve ser atribuído a qualquer operação que afecte o espaço de trabalho IDE em geral. Este contexto é utilizado pelas vistas que manipulam o espaço de trabalho como, por exemplo, o Navegador de Recursos. A área de trabalho IDE instala um adaptador no espaço de trabalho para IUndoContext que devolve o contexto de anulações da área de trabalho. Este registo baseado em modelos permite aos plug-ins que manipulam o espaço de trabalho obterem o contexto de anulações apropriado, mesmo que não tenham registo e não referenciem nenhumas classes de área de trabalho.

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

Os outros plug-ins são encorajados a usar esta mesma técnica para registar contextos de anulação baseados em modelos.