Операции с возможностью отмены

Ранее были рассмотрены различные способы добавления действий в рабочую среду, однако реализации метода run() действия внимание до сих пор не уделялось. Реализация метода зависит от конкретного рассматриваемого действия, однако если структура исходного кода соответствует определению операции с возможностью отмены, то это действие может принимать участие в операциях отмены и повтора платформы.

Платформа предоставляет среду операций с возможностью отмены в пакете org.eclipse.core.commands.operations. После добавления IUndoableOperation в метод run() операция становится доступной для отмены и повтора. Сам процесс преобразования действия достаточно прост; основная сложность заключается в реализации операций отмены и повтора.

Создание операции с возможностью отмены

Начнем с рассмотрения простого примера. Вспомните простой ViewActionDelegate из примера модуля readme. При вызове действие просто запускает окно диалога, сообщающее о его выполнении.

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) {
	// обработка исключительной ситуации
}
Хронология получит последнюю операцию с указанным контекстом и запросит ее отмену. С помощью другого протокола можно запросить всю хронологию отмены или повтора для контекста, либо найти операцию для отмены или повтора в рамках конкретного контекста. Следующий фрагмент кода позволяет получить метку операции, подлежащей отмене в рамках конкретного контекста.
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);

Каждый раз при выполнении, отмене или повторе операции с помощью протокола хронологии операций клиенты могут предоставить монитор состояния, а также дополнительную информацию о пользовательском интерфейсе, которая может потребоваться для выполнения операции. Такая информация передается непосредственно операции. В нашем исходном примере действие readme создает операцию с параметром оболочки, с помощью которой можно открыть окно диалога. Вместо сохранения оболочки в операции параметры можно передать методам выполнения, отмены или повтора, которые предоставляют необходимую информацию о пользовательском интерфейсе. Эти параметры передаются непосредственно операции.

public void run(org.eclipse.jface.action.IAction action) {
	IUndoableOperation operation = new ReadmeOperation();
	...
	operationHistory.execute(operation, null, infoAdapter);
}
infoAdapter представляет собой IAdaptable, предоставляющий 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;
		}
	}
	// дополнительные действия...
}

Обработчики действий отмены и повтора

Платформа предоставляет стандартные обработчики действий отмены и повтора с возможность изменения целевого объекта. Они могут настраиваться панелями и редакторами для обеспечения поддержки операций отмены и повтора рамках в конкретных контекстов.При создании обработчику присваивается контекст, в соответствии с которым выполняется фильтрация хронологии операций. Обработчики действий отвечают за обновление меток отмены и повтора, с помощью которых определяется текущая операция, за предоставление монитора состояния и информации о пользовательском интерфейсе для хронологии операций, а также за усечение хронологии в случае недопустимости текущей операции. Для удобства предусмотрена группа действий, с помощью которой можно создавать обработчики действий и присваивать их глобальным действиям отмены и повтора.

new UndoRedoActionGroup(this.getSite(), undoContext, true);
Последний параметр представляет собой булевское значение, указывающее на необходимость очистки хронологий отмены и повтора указанного контекста в случае недопустимости текущей операции. Этот параметр следует указывать в соответствии с предоставленным контекстом отмены, а также стратегией проверки, применяемой операциями в рамках этого контекста.

Модели отмены приложений

Ранее было показано, каким образом контексты отмены позволяют реализовать различные типы моделей отмены приложений. Возможность указания для операций нескольких контекстов позволяет приложениям реализовывать стратегии отмены, которые могут быть строго ограничены отдельными панелями и редакторами, строго глобальные модели на уровне всех модулей, а также промежуточные модели. Кроме того, при разработке операций отмены и повтора следует определить, можно ли отменить или восстановить произвольную операцию в любой момент, а также использовать ли строго линейную модель (для отмены или повтора доступна только одна операция).

IOperationHistory задает протокол, позволяющий реализовать гибкие модели отмены, в соответствии с которыми все ограничения устанавливаются конкретными реализациями. Протокол отмены и повтора, рассмотренный прежде, предполагает, что в конкретном контексте для отмены или повтора доступна только одна операция. Дополнительный протокол позволяет клиентам выполнять конкретные операции независимо от их расположения в хронологии. Хронологию операций можно настроить таким образом, чтобы реализовать модель, соответствующую особенностями приложения. Для этого применяется интерфейс, обеспечивающий предварительное утверждение запроса на отмену или повтор перед фактической отменой или повтором операции.

Средства проверки операций

IOperationApprover задает протокол проверки отмены или повтора операций. Средство проверки операций устанавливается поверх хронологии операций. Средства проверки операций могут проверять все операции, операции, принадлежащие только определенному контексту, либо не выполнять проверку вообще и сообщать пользователю об ошибках в операции при обращении к ней. Следующий фрагмент кода показывает, каким образом приложение может настроить хронологию операций для принудительного применения линейной модели отмены для всех операция.
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// устанавливается средство проверки операций, разрешающее отмену только самых последних операций
history.addOperationApprover(new LinearUndoEnforcer());

В этом случае средство проверки операций LinearUndoEnforcer, представленное средой, разрешает отмену и повтор только самых последних операций для всех контекстов.

LinearUndoViolationUserApprover разрешает отмену и повтор самых последних операций и запрашивает подтверждение пользователя при отмене или повторе других. Это средство проверки операций может быть установлено на определенной части рабочей среды.

IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// устанавливается средство проверки операций, запрашивающее подтверждение для всех операций, кроме самых последних
IOperationApprover approver = new LinearUndoViolationUserApprover(myUndoContext, myWorkbenchPart);
history.addOperationApprover(approver);

Разработчики модулей могут создавать и устанавливать собственные средства проверки операций, реализующие нестандартные модели отмены.

Отмена и рабочая среда IDE

Выше были приведены фрагменты кода, в которых для обращения к хронологии операций и контексту отмены рабочей среды применяется протокол рабочей среды. Для этой цели используется IWorkbenchOperationSupport, предоставляемый рабочей средой. Понятие контекста отмены уровня рабочей среды достаточно общее. Область действия контекста отмены рабочей среды, а также панели и редакторы, применяющие этот контекст для обеспечения поддержки операций отмены, определяются приложением рабочей среды.

В случае рабочей среды Eclipse IDE контекст следует присваивать всем операциям, изменяющим рабочую область IDE. Этот контекст применяется такими панелями управляющими рабочей областью, как Навигатор ресурсов. Рабочая среда IDE устанавливает адаптер в рабочей области для IUndoContext, который возвращает контекст отмены рабочей среды. Такой подход позволяет модулям, управляющим рабочей областью, получить подходящий контекст отмены даже в том случае, если в них отсутствует заголовок и они не связаны с классами рабочей среды.

// получение хронологии операций
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// получение контекста отмены для модели
IUndoContext workspaceContext = (IUndoContext)ResourcesPlugin.getWorkspace().getAdapter(IUndoContext.class);
if (workspaceContext != null) {
	// создание операции и присваивание ей контекста
}

Эту технику рекомендуется использовать в других модулях для регистрации контекстов отмены различных моделей.