/* * 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); } }); } }