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