Для создателей инструментов платформа Eclipse предоставляет широкие возможности для привязки ключей к командам, так что конкретные действия могут быть вызваны с помощью правильной комбинации клавиш. В большинстве случаев подключиться к этим средствам — просто активировать правильный контекст через IContextService и использовать несколько точек расширения в вашем plugin.xml. Для более сложных редакторов, например, с несколькими элементами управления Text или StyledText, это не так просто. В этой статье рассматриваются механизмы, используемые платформой Eclipse для определения способа обработки команд, и описывается подход к реализации редакторов, которые хотят изменить обработку команд в зависимости от элемента управления фокусом.
Предположим, мы создаем редактор, который имеет более одного элемента управления StyledText и использует IOperationHistory для поддержки отмены / повтора. Мы хотим, чтобы CTRL + Z и CTRL + Y (Command-Z и Command-Y на Mac) вызывали отмену / повтор в редакторе. В этом редакторе мы хотим, чтобы эти ключи управляли IOperationHistory, однако, когда StyledText имеет фокус в нашем редакторе, мы хотим вызвать отмену / повтор в StyledText. Другими словами, пользователь будет ожидать, что отмена / повтор будет вести себя по-разному в зависимости от того, что они делают. Это согласуется с тем, как отмены / повторы работают на остальной части платформы.
Eclipse использует следующие абстракции для представления концепции ключей, вызывающих команды:
- KeyBinding — связывает последовательность ключей с логической командой
- Команда — абстрактное представление для некоторого семантического поведения
- IHandler — реализация поведения для конкретной команды
Давайте возьмем (например) CTRL + Z. Когда нажимаются клавиши, они сопоставляются с привязкой клавиш. Eclipse знает, какую команду вызывать, основываясь на связывании клавиш (в данном случае это команда «отменить»). Затем Eclipse определяет, какой IHandler использовать, путем поиска обработчика с помощью логической команды в IHandlerService. (Существует много уровней косвенности в реальной реализации — этот пример упрощен для простоты понимания).
В нашем редакторе мы хотим, чтобы история операций была связана с отменой / повторением. Мы подключаем его следующим образом:
OperationHistoryActionHandler undoAction= new UndoActionHandler(site, moduleEditor.getOperationContext());
PlatformUI.getWorkbench().getHelpSystem().setHelp(undoAction, IAbstractTextEditorHelpContextIds.UNDO_ACTION);
undoAction.setActionDefinitionId(IWorkbenchActionDefinitionIds.UNDO);
undoAction.setId(ITextEditorActionConstants.UNDO);
IHandlerService handlerService = (IHandlerService) site.getService(IHandlerService.class);
handlerService.activateHandler(undoAction.getActionDefinitionId(), new ActionHandler(undoAction));
Когда мы тестируем наш редактор, это работает должным образом: мы можем отменить изменения в редакторе, нажав CTRL + Z. Однако после того, как мы сосредоточимся на StyledText в нашем редакторе, мы ожидаем, что CTRL + Z отменит текст, который мы набираем. В нашем редакторе этих текстовых изменений нет в нашей истории операций, пока текстовый элемент управления не потерял фокус. Так как мы это настроим? Вот как:
TextViewer textViewer = // create the text viewer
TextViewerSupport support = new TextViewerSupport(textViewer);
Большая часть работы выполняется этим вспомогательным классом:
protected class TextViewerSupport implements FocusListener, DisposeListener {
private final TextViewer textViewer;
private List handlerActivations = new ArrayList();
public TextViewerSupport(TextViewer textViewer) {
this.textViewer = textViewer;
StyledText textWidget = textViewer.getTextWidget();
textWidget.addFocusListener(this);
textWidget.addDisposeListener(this);
if (textViewer.getTextWidget().isFocusControl()) {
activateContext();
}
}
public void focusLost(FocusEvent e) {
deactivateContext();
}
public void focusGained(FocusEvent e) {
activateContext();
}
public void widgetDisposed(DisposeEvent e) {
deactivateContext();
}
protected void activateContext() {
if (handlerActivations.isEmpty()) {
activateHandler(ISourceViewer.QUICK_ASSIST,ITextEditorActionDefinitionIds.QUICK_ASSIST);
activateHandler(ISourceViewer.CONTENTASSIST_PROPOSALS,ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS);
activateHandler(ITextOperationTarget.CUT, ITextEditorActionDefinitionIds.CUT);
activateHandler(ITextOperationTarget.COPY, ITextEditorActionDefinitionIds.COPY);
activateHandler(ITextOperationTarget.PASTE, ITextEditorActionDefinitionIds.PASTE);
activateHandler(ITextOperationTarget.DELETE, ITextEditorActionDefinitionIds.DELETE);
activateHandler(ITextOperationTarget.UNDO, ITextEditorActionDefinitionIds.UNDO);
activateHandler(ITextOperationTarget.REDO, ITextEditorActionDefinitionIds.REDO);
}
}
protected void activateHandler(int operation, String actionDefinitionId) {
StyledText textWidget = textViewer.getTextWidget();
IHandler actionHandler = createActionHandler(operation, actionDefinitionId);
IHandlerActivation handlerActivation = handlerService.activateHandler(actionDefinitionId, actionHandler,new ActiveFocusControlExpression(textWidget));
handlerActivations.add(handlerActivation);
}
private IHandler createActionHandler(final int operation, String actionDefinitionId) {
Action action = new Action() {
@Override
public void run() {
if (textViewer.canDoOperation(operation)) {
textViewer.doOperation(operation);
}
}
};
action.setActionDefinitionId(actionDefinitionId);
return new ActionHandler(action);
}
protected void deactivateContext() {
if (!handlerActivations.isEmpty()) {
for (IHandlerActivation activation: handlerActivations) {
handlerService.deactivateHandler(activation);
activation.getHandler().dispose();
}
handlerActivations.clear();
}
}
}
Хотя TextViewerSupport выглядит сложным, на самом деле он просто активирует различные обработчики команд, когда textViewer получает фокус.
Когда текстовый элемент управления имеет фокус, редактор теперь имеет два обработчика для команды отмены. Итак, как Eclipse знает, какой использовать? Изучение внутренних частей Eclipse показывает, что он использует обработчик с наивысшим приоритетом. Но мы не устанавливаем приоритет этим обработчикам! Eclipse автоматически вычисляет приоритет обработчиков на основе выражения включения обработчика. Приоритет выражения включения частично зависит от переменных, на которые ссылается выражение. Это приводит нас к последней части нашего решения, ActiveFocusControlExpression:
/**
* An expression that evaluates to true if and only if the current focus control is the one provided.
* Has a very high priority in order to ensure proper conflict resolution.
*/
public class ActiveFocusControlExpression extends Expression {
private Control focusControl;
public ActiveFocusControlExpression(Control control) {
focusControl = control;
}
@Override
public void collectExpressionInfo(ExpressionInfo info) {
info.markDefaultVariableAccessed(); // give it a very high priority
info.addVariableNameAccess(ISources.ACTIVE_SHELL_NAME);
info.addVariableNameAccess(ISources.ACTIVE_WORKBENCH_WINDOW_NAME);
}
@Override
public EvaluationResult evaluate(IEvaluationContext context)
throws CoreException {
if (Display.getCurrent() != null && focusControl.isFocusControl()) {
return EvaluationResult.TRUE;
}
return EvaluationResult.FALSE;
}
}
В collectExpressionInfo мы гарантируем, что выражение указывает, что оно использует переменную по умолчанию. Это дает выражению очень высокий приоритет. Поскольку выражение доступно только тогда, когда наш элемент управления имеет фокус, мы убедились, что отмена направлена на наш текстовый элемент управления в нужное время.
Кредит: подход, описанный в этой статье, основан на классе CommonTextSupport (автор Steffen Pingel) из проекта Mylyn .