/* * 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.selection; import jetbrains.mps.classloading.ClassLoaderManager; import jetbrains.mps.editor.runtime.commands.EditorCommand; import jetbrains.mps.editor.runtime.impl.cellActions.CommentMultipleNodesAction; import jetbrains.mps.editor.runtime.selection.SelectionUtil; import jetbrains.mps.openapi.editor.EditorComponent; import jetbrains.mps.openapi.editor.EditorContext; import jetbrains.mps.openapi.editor.cells.CellAction; import jetbrains.mps.openapi.editor.cells.CellActionType; import jetbrains.mps.openapi.editor.cells.CellInfo; import jetbrains.mps.openapi.editor.cells.EditorCell; import jetbrains.mps.openapi.editor.selection.MultipleSelection; import jetbrains.mps.openapi.editor.selection.Selection; import jetbrains.mps.openapi.editor.selection.SelectionInfo; import jetbrains.mps.openapi.editor.selection.SelectionManager; import jetbrains.mps.openapi.editor.selection.SelectionStoreException; import jetbrains.mps.persistence.PersistenceRegistry; import jetbrains.mps.smodel.ModelAccessHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeId; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; public class NodeRangeSelection extends AbstractMultipleSelection implements MultipleSelection { private static final String ROLE_PROPERTY_NAME = "role"; private static final String MODEL_ID_PROPERTY_NAME = "modelId"; private static final String FIRST_NODE_ID_PROPERTY_NAME = "firstNodeId"; private static final String LAST_NODE_ID_PROPERTY_NAME = "lastNodeId"; private static final String PARENT_NODE_ID_PROPERTY_NAME = "parentNodeId"; private static final String SELECTION_FILTER_CLASS_NAME = "selectionFilterClassName"; private static final String SELECTION_FILTER_MODULE_REFERENCE = "selectionFilterModuleId"; private static final String EMPTY_CELL_ID = "emptyCellId"; private final SNode myFirstNode; private final SNode myLastNode; private final SNode myParentNode; private final String myRole; private final String myModelReference; private final RangeSelectionFilter myFilter; private final String myEmptyCellId; public NodeRangeSelection(@NotNull EditorComponent editorComponent, Map<String, String> properties, CellInfo cellInfo) throws SelectionStoreException, SelectionRestoreException { super(editorComponent); if (cellInfo != null) { throw new SelectionStoreException("Non-null CellInfo object passed as a parameter: " + cellInfo); } myRole = properties.get(ROLE_PROPERTY_NAME); if (myRole == null) { throw new SelectionStoreException("Role property missed"); } myModelReference = properties.get(MODEL_ID_PROPERTY_NAME); if (myModelReference == null) { throw new SelectionStoreException("Model ID property missed"); } SModel sModelDescriptor; try { final SModelReference modelRef = PersistenceFacade.getInstance().createModelReference(myModelReference); sModelDescriptor = modelRef.resolve(editorComponent.getEditorContext().getRepository()); } catch (Exception ex) { SelectionRestoreException sre = new SelectionRestoreException(); sre.initCause(ex); throw sre; } if (sModelDescriptor == null) { throw new SelectionRestoreException(); } myFirstNode = findNode(sModelDescriptor, properties, FIRST_NODE_ID_PROPERTY_NAME); myLastNode = findNode(sModelDescriptor, properties, LAST_NODE_ID_PROPERTY_NAME); myParentNode = findNode(sModelDescriptor, properties, PARENT_NODE_ID_PROPERTY_NAME); if (myParentNode != myFirstNode.getParent() || myParentNode != myLastNode.getParent()) { throw new SelectionRestoreException(); } if (!myRole.equals(myFirstNode.getRoleInParent()) || !myRole.equals(myLastNode.getRoleInParent())) { throw new SelectionRestoreException(); } myFilter = createSelectionFilter(properties); myEmptyCellId = properties.get(EMPTY_CELL_ID); try { initSelectedCells(); } catch (CellNotFoundException e) { throw new SelectionRestoreException(); } } public NodeRangeSelection(@NotNull EditorComponent editorComponent, @NotNull SNode firstNode, @NotNull SNode lastNode) { this(editorComponent, firstNode, lastNode, null, null); } public NodeRangeSelection(@NotNull EditorComponent editorComponent, @NotNull SNode firstNode, @NotNull SNode lastNode, RangeSelectionFilter filter, String emptyCellId) { super(editorComponent); myFirstNode = firstNode; myLastNode = lastNode; myParentNode = myFirstNode.getParent(); myRole = myFirstNode.getRoleInParent(); myModelReference = myFirstNode.getModel().getReference().toString(); myFilter = filter; myEmptyCellId = emptyCellId; assert myParentNode != null; assert myParentNode == myLastNode.getParent(); assert myRole != null && myRole.equals( myLastNode.getRoleInParent()) : "First node role: " + myRole + ", last node role: " + myLastNode.getRoleInParent(); try { initSelectedCells(); } catch (CellNotFoundException e) { assert false : "EditorCell was not found for node: " + e.getNode(); } } private void initSelectedCells() throws CellNotFoundException { List<EditorCell> selectedCells = new ArrayList<EditorCell>(); boolean withinSelection = false; boolean breakLoop = false; for (SNode child : getChildIterable()) { if (myFirstNode.equals(child) || myLastNode.equals(child)) { if (withinSelection || myFirstNode.equals(myLastNode)) { breakLoop = true; } if (!withinSelection) { withinSelection = true; } } if (withinSelection) { EditorCell editorCell = getEditorComponent().findNodeCell(child); if (editorCell == null) { throw new CellNotFoundException(child); } selectedCells.add(editorCell); } if (breakLoop) { break; } } // asserting both first/last node was in this children collection assert withinSelection; assert breakLoop; setSelectedCells(selectedCells); } @Override public SelectionInfo getSelectionInfo() { SelectionInfoImpl selectionInfo = new SelectionInfoImpl(this.getClass().getName()); selectionInfo.getPropertiesMap().put(ROLE_PROPERTY_NAME, myRole); selectionInfo.getPropertiesMap().put(MODEL_ID_PROPERTY_NAME, myModelReference); selectionInfo.getPropertiesMap().put(FIRST_NODE_ID_PROPERTY_NAME, myFirstNode.getNodeId().toString()); selectionInfo.getPropertiesMap().put(LAST_NODE_ID_PROPERTY_NAME, myLastNode.getNodeId().toString()); selectionInfo.getPropertiesMap().put(PARENT_NODE_ID_PROPERTY_NAME, myParentNode.getNodeId().toString()); if (myFilter != null) { selectionInfo.getPropertiesMap().put(SELECTION_FILTER_CLASS_NAME, myFilter.getClass().getName()); selectionInfo.getPropertiesMap().put(SELECTION_FILTER_MODULE_REFERENCE, myFilter.getModuleReference()); myFilter.saveFilter(selectionInfo); } if (myEmptyCellId != null) { selectionInfo.getPropertiesMap().put(EMPTY_CELL_ID, myEmptyCellId); } return selectionInfo; } @Override public boolean isSame(Selection another) { if (this == another) { return true; } if (another == null || getClass() != another.getClass()) { return false; } NodeRangeSelection that = (NodeRangeSelection) another; return myFirstNode.equals(that.myFirstNode) && myLastNode.equals(that.myLastNode) && myParentNode.equals(that.myParentNode) && myRole.equals(that.myRole); } @Override public boolean canExecuteAction(CellActionType type) { return type == CellActionType.BACKSPACE || type == CellActionType.DELETE || super.canExecuteAction(type); } @Override public void executeAction(CellActionType type) { ((jetbrains.mps.nodeEditor.EditorComponent) getEditorComponent()).assertModelNotDisposed(); if (type == CellActionType.BACKSPACE || type == CellActionType.DELETE) { performDeleteAction(type); return; } else if (type == CellActionType.COMMENT) { CommentMultipleNodesAction action = new CommentMultipleNodesAction(getSelectedNodes()); EditorContext editorContext = getEditorComponent().getEditorContext(); if (canExecute(editorContext, action)) { action.execute(editorContext); } } super.executeAction(type); } private RangeSelectionFilter createSelectionFilter(Map<String, String> properties) throws SelectionStoreException, SelectionRestoreException { String filterClassName = properties.get(SELECTION_FILTER_CLASS_NAME); if (filterClassName == null) { return null; } String moduleRefString = properties.get(SELECTION_FILTER_MODULE_REFERENCE); RangeSelectionFilter result; try { Class filterClass = moduleRefString != null ? loadFromModule(moduleRefString, filterClassName) : Class.forName(filterClassName); if (filterClass == null) { throw new SelectionStoreException( "Can't load selection filter class: " + filterClassName + (moduleRefString != null ? "" : "module reference: " + moduleRefString)); } Object filterInstance = filterClass.newInstance(); if (filterInstance instanceof RangeSelectionFilter) { result = (RangeSelectionFilter) filterInstance; } else { throw new SelectionStoreException("Loaded filter class " + filterInstance + " is not instance of RangeSelectionFilter"); } } catch (ClassNotFoundException e) { throw new SelectionStoreException("Filter class not found: " + e.getMessage()); } catch (InstantiationException e) { throw new SelectionStoreException("Can't instantiate filter class: " + e.getMessage()); } catch (IllegalAccessException e) { throw new SelectionStoreException("Illegal access while instantiating filter class: " + e.getMessage()); } result.loadFilter(properties); return result; } private Class loadFromModule(String moduleRefString, String className) throws SelectionStoreException { SModuleReference moduleReference = PersistenceRegistry.getInstance().createModuleReference(moduleRefString); if (moduleReference == null) { throw new SelectionStoreException("Can't parse module reference: " + moduleRefString); } SModule module = moduleReference.resolve(getEditorComponent().getEditorContext().getRepository()); if (module == null) { throw new SelectionStoreException("Can't find module: " + moduleRefString + " in the repository"); } return ClassLoaderManager.getInstance().getOwnClass(module, className); } @NotNull private SNode findNode(SModel sModel, Map<String, String> properties, String propertyName) throws SelectionStoreException, SelectionRestoreException { String sNodeId = properties.get(propertyName); if (sNodeId == null) { throw new SelectionStoreException("Required node Id property missed, propertyName = " + propertyName); } SNodeId nodeId = PersistenceFacade.getInstance().createNodeId(sNodeId); assert nodeId != null : "wrong node id string"; SNode sNode = sModel.getNode(nodeId); if (sNode == null) { throw new SelectionRestoreException(); } return sNode; } private void performDeleteAction(final CellActionType type) { // TODO: handle delete action similar to all other actions (using corresponding editor component action) final EditorContext editorContext = getEditorComponent().getEditorContext(); int selectedCellsSize = getSelectedCells().size(); if (selectedCellsSize > 1) { editorContext.getRepository().getModelAccess().executeCommand(new EditorCommand(editorContext) { @Override public void doExecute() { List<SNode> selectedNodes = getSelectedNodes(); assert selectedNodes.size() > 1; SNode prevSelectableNode = getNextSelectableNode(selectedNodes.get(0), false); SNode nextSelectableNode = getNextSelectableNode(selectedNodes.get(selectedNodes.size() - 1), true); for (SNode node : selectedNodes) { node.delete(); } switch (type) { case BACKSPACE: if (selectNode(prevSelectableNode, false) || selectNode(nextSelectableNode, true)) { return; } break; case DELETE: if (selectNode(nextSelectableNode, true) || selectNode(prevSelectableNode, false)) { return; } break; default: assert false : "Incorrect acton type passed: " + type; } // selecting default cell - no children found. if (myEmptyCellId != null) { SelectionUtil.selectLabelCellAnSetCaret(editorContext, myParentNode, myEmptyCellId, 0); return; } EditorCell emptyCell = getEditorComponent().findNodeCellWithRole(myParentNode, myRole); if (emptyCell != null) { assert myParentNode.getModel() != null : "The model of the parent node (" + myParentNode + ") in this selection is null."; assert emptyCell.getSNode().getModel() != null : "The model is null for emptyCell node (" + emptyCell.getSNode() + ")"; editorContext.selectWRTFocusPolicy(emptyCell); return; } SelectionUtil.selectLabelCellAnSetCaret(editorContext, myParentNode, SelectionManager.LAST_CELL, -1); } }); } else if (selectedCellsSize == 1) { EditorCell nodeCell = getFirstCell(); final CellAction action = nodeCell.getAction(type); if (action == null) { return; } if (!canExecute(editorContext, action)) { return; } if (action.executeInCommand()) { editorContext.getRepository().getModelAccess().executeCommand(new EditorCommand(editorContext) { @Override public void doExecute() { action.execute(editorContext); } }); } else { action.execute(editorContext); } } } private boolean canExecute(final EditorContext editorContext, final CellAction action) { return new ModelAccessHelper(editorContext.getRepository()).runReadAction(() -> action.canExecute(editorContext)); } private boolean selectNode(SNode node, boolean startPosition) { if (node != null) { SelectionUtil.selectLabelCellAnSetCaret(getEditorComponent().getEditorContext(), node, startPosition ? SelectionManager.FIRST_CELL : SelectionManager.LAST_CELL, startPosition ? 0 : -1); } return node != null; } // TODO: enlargeSelection action should be handled specifically by executeAction() method public NodeRangeSelection enlargeSelection(boolean next) { SNode newLastNode = getNextSelectableNode(myLastNode, next); return newLastNode != null ? new NodeRangeSelection(getEditorComponent(), myFirstNode, newLastNode, myFilter, myEmptyCellId) : null; } private SNode getNextSelectableNode(SNode anchorNode, boolean forward) { SNode prevNode = null; for (SNode child : getChildIterable()) { if (forward && prevNode == anchorNode) { return child; } if (!forward && child == anchorNode) { return prevNode; } prevNode = child; } return null; } private Iterable<? extends SNode> getChildIterable() { if (myFilter == null) { return myParentNode.getChildren(myRole); } List<SNode> result = new LinkedList<SNode>(); for (SNode nextChild : myParentNode.getChildren(myRole)) { if (myFilter.accept(nextChild)) { result.add(nextChild); } } return result; } @Override public void ensureVisible() { EditorCell lastCellToSelect = getEditorComponent().findNodeCell(myLastNode); assert lastCellToSelect != null; getEditorComponent().scrollToCell(lastCellToSelect); } public static abstract class RangeSelectionFilter { public abstract boolean accept(SNode node); public void saveFilter(SelectionInfo info) { } /** * @return SModuleReference.toString() or null if corresponding filter class can be loaded using NodeRangeSelection classloader. */ public abstract String getModuleReference(); public void loadFilter(Map<String, String> properties) throws SelectionRestoreException, SelectionStoreException { } } private static class CellNotFoundException extends Exception { private SNode myNode; private CellNotFoundException(SNode node) { super("EditorCell was not found, node: " + node); myNode = node; } public SNode getNode() { return myNode; } } }