/*
* Copyright 2003-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.nodeEditor.keymaps;
import jetbrains.mps.editor.runtime.commands.EditorCommand;
import jetbrains.mps.editor.runtime.impl.LanguagesKeymapManager;
import jetbrains.mps.editor.runtime.style.StyleAttributesUtil;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.KeyMap;
import jetbrains.mps.openapi.editor.cells.KeyMap.ActionKey;
import jetbrains.mps.openapi.editor.cells.KeyMapAction;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.smodel.SLanguageHierarchy;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.Pair;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SModule;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* User: shatalin
* Date: 2/5/13
*/
public abstract class KeymapHandler<E> {
private static final Logger LOG = LogManager.getLogger(KeymapHandler.class);
public Collection<KeyMapAction> getAllRegisteredActions(EditorCell selectedCell, EditorContext context) {
Set<KeyMapAction> result = new HashSet<KeyMapAction>();
for (Pair<KeyMap, EditorCell> pair : getRegisteredKeymaps(selectedCell, context)) {
result.addAll(pair.o1.getAllActions());
}
return result;
}
public Collection<Pair<KeyMapAction, EditorCell>> getActions(final EditorCell selectedCell, E event, final EditorContext context) {
final Collection<ActionKey> actionKeys = getActionKeys(event);
assert !actionKeys.isEmpty();
return ModelAccess.instance().runReadAction(new Computable<List<Pair<KeyMapAction, EditorCell>>>() {
@Override
public List<Pair<KeyMapAction, EditorCell>> compute() {
// collect all keymaps available
List<Pair<KeyMap, EditorCell>> keymapsAndCells = getRegisteredKeymaps(selectedCell, context);
if (keymapsAndCells.isEmpty()) {
return Collections.emptyList();
}
return selectActionsFromKeymaps(selectedCell, actionKeys, context, keymapsAndCells);
}
});
}
public abstract Collection<ActionKey> getActionKeys(E event);
public void executeAction(final KeyMapAction action, EditorCell contextCell, final EditorContext editorContext) {
editorContext.runWithContextCell(contextCell, new Runnable() {
@Override
public void run() {
editorContext.getRepository().getModelAccess().executeCommand(new EditorCommand(editorContext) {
@Override
public void doExecute() {
action.execute(editorContext);
}
});
}
});
}
public abstract void showActionsMenu(Collection<Pair<KeyMapAction, EditorCell>> actionsInfo, EditorContext editorContext, EditorCell selectedCell);
/**
* @return List of pairs keymap/ownerCell
*/
private List<Pair<KeyMap, EditorCell>> getRegisteredKeymaps(EditorCell selectedCell, EditorContext editorContext) {
Set<Class> addedKeymaps = new HashSet<Class>(); // don't duplicate keymaps
List<Pair<KeyMap, EditorCell>> keyMapsAndCells = new ArrayList<Pair<KeyMap, EditorCell>>();
EditorCell keymapOwnerCell = selectedCell;
while (keymapOwnerCell != null) {
KeyMap keymap = keymapOwnerCell.getKeyMap();
if (keymap != null && !addedKeymaps.contains(keymap.getClass())) {
keyMapsAndCells.add(new Pair<KeyMap, EditorCell>(keymap, keymapOwnerCell));
addedKeymaps.add(keymap.getClass());
}
keymapOwnerCell = keymapOwnerCell.getParent();
}
SModel model = editorContext.getModel();
if (model != null) {
// As long as our concept hierarchy mimics that of languages, it's ok to go through extended languages
// to find out possible editors/keymaps declared for super-concepts. This code has to change into generated
// factory for keymaps, so that we don't need to walk hierarchy here.
for (SLanguage l : new SLanguageHierarchy(SModelOperations.getAllLanguageImports(model)).getExtended()) {
List<KeyMap> keyMapsForNamespace = LanguagesKeymapManager.getInstance().getKeyMapsForLanguage(l);
if (keyMapsForNamespace != null) {
for (KeyMap keymap : keyMapsForNamespace) {
if (!addedKeymaps.contains(keymap.getClass())) {
keyMapsAndCells.add(new Pair<KeyMap, EditorCell>(keymap, selectedCell));
addedKeymaps.add(keymap.getClass());
}
}
}
}
}
return keyMapsAndCells;
}
/**
* @param keymapsAndCells - List of pairs keymap/ownerCell
* @return List of pairs keymapAction/actionCell
*/
private List<Pair<KeyMapAction, EditorCell>> selectActionsFromKeymaps(EditorCell selectedCell, Collection<ActionKey> actionKeys, EditorContext editorContext,
List<Pair<KeyMap, EditorCell>> keymapsAndCells) {
// choose appropriate actions from keymaps
List<Pair<KeyMapAction, EditorCell>> actionsAndCells = new LinkedList<Pair<KeyMapAction, EditorCell>>();
for (Pair<KeyMap, EditorCell> keymapAndCell : keymapsAndCells) {
KeyMap keymap = keymapAndCell.o1;
EditorCell keymapOwnerCell = keymapAndCell.o2;
int caretPosition = getCaretPositionCode(selectedCell, keymapOwnerCell);
Collection<KeyMapAction> actions = keymap.getActions(actionKeys);
for (KeyMapAction action : actions) {
EditorCell actionCell = selectActionCell(action, keymapOwnerCell, selectedCell, caretPosition, editorContext);
if (actionCell != null) {
actionsAndCells.add(new Pair<KeyMapAction, EditorCell>(action, actionCell));
}
}
}
return actionsAndCells;
}
private int getCaretPositionCode(EditorCell selectedCell, EditorCell keyMapCell) {
int actualCaretPosition = KeyMapAction.CARET_AT_ANY_POSITION;
if (selectedCell instanceof EditorCell_Label) {
EditorCell_Label label = (EditorCell_Label) selectedCell;
if (isStrictlyFirstCaretPosition(label) && CellTraversalUtil.getFirstLeaf(keyMapCell) == selectedCell) {
actualCaretPosition = KeyMapAction.CARET_AT_FIRST_POSITION;
} else if (isStrictlyLastCaretPosition(label) && CellTraversalUtil.getLastLeaf(keyMapCell) == selectedCell) {
actualCaretPosition = KeyMapAction.CARET_AT_LAST_POSITION;
} else {
actualCaretPosition = KeyMapAction.CARET_AT_INTERMEDIATE_POSITION;
}
}
return actualCaretPosition;
}
private static boolean isStrictlyFirstCaretPosition(EditorCell_Label label) {
return label.isFirstCaretPosition() && StyleAttributesUtil.isFirstPositionAllowed(label.getStyle()) &&
CellTraversalUtil.getFirstLeaf(CellTraversalUtil.getContainingBigCell(label)) == label;
}
private static boolean isStrictlyLastCaretPosition(EditorCell_Label label) {
return label.isLastCaretPosition() && StyleAttributesUtil.isLastPositionAllowed(label.getStyle()) &&
CellTraversalUtil.getLastLeaf(CellTraversalUtil.getContainingBigCell(label)) == label;
}
private EditorCell selectActionCell(KeyMapAction action, EditorCell keymapOwnerCell, EditorCell selectedCell, int actualCaretPosition,
EditorContext editorContext) {
// Action caret policy is different from actual one
if (action.getCaretPolicy() != KeyMapAction.CARET_AT_ANY_POSITION && action.getCaretPolicy() != actualCaretPosition) {
return null;
}
// choose 'context cell' (between 'selected cell' and 'keymap owner cell' inclusive)
for (EditorCell actionCell = selectedCell; actionCell != null; actionCell = actionCell.getParent()) {
try {
if (canExecuteKeyMapAction(action, actionCell, editorContext)) {
return actionCell;
}
} catch (Exception e) {
LOG.error(null, e);
return null;
}
if (actionCell == keymapOwnerCell) {
break;
}
}
return null;
}
private boolean canExecuteKeyMapAction(final KeyMapAction action, EditorCell contextCell, final EditorContext editorContext) {
return editorContext.runWithContextCell(contextCell, new Computable<Boolean>() {
@Override
public Boolean compute() {
return action.canExecute(editorContext);
}
});
}
}