/*
* Copyright 2003-2016 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.cellActions;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import jetbrains.mps.datatransfer.PasteNodeData;
import jetbrains.mps.datatransfer.PastePlaceHint;
import jetbrains.mps.editor.runtime.cells.AbstractCellAction;
import jetbrains.mps.editor.runtime.commands.EditorCommand;
import jetbrains.mps.ide.datatransfer.CopyPasteUtil;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.nodeEditor.ChildrenCollectionFinder;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.nodeEditor.cells.GeometryUtil;
import jetbrains.mps.nodeEditor.datatransfer.NodePaster;
import jetbrains.mps.nodeEditor.datatransfer.NodePaster.NodeAndRole;
import jetbrains.mps.nodeEditor.selection.EditorCellLabelSelection;
import jetbrains.mps.nodeEditor.selection.EditorCellSelection;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.CellConditions;
import jetbrains.mps.openapi.editor.cells.CellInfo;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;
import jetbrains.mps.openapi.editor.selection.MultipleSelection;
import jetbrains.mps.openapi.editor.selection.Selection;
import jetbrains.mps.openapi.editor.selection.SelectionManager;
import jetbrains.mps.openapi.editor.selection.SingularSelection;
import jetbrains.mps.project.Project;
import jetbrains.mps.resolve.ResolverComponent;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SRepository;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Author: Sergey Dmitriev.
* Time: Nov 26, 2003 2:06:41 PM
*/
public class CellAction_PasteNode extends AbstractCellAction {
private static final Logger LOG = Logger.wrap(LogManager.getLogger(CellAction_PasteNode.class));
@Override
public boolean canExecute(EditorContext context) {
Selection selection = context.getSelectionManager().getSelection();
List<SNode> selectedNodes = selection.getSelectedNodes();
if (selectedNodes.isEmpty()) {
return false;
}
List<SNode> pasteNodes = CopyPasteUtil.getNodesFromClipboard(selectedNodes.get(0).getModel());
if (pasteNodes == null || pasteNodes.isEmpty()) {
// it used to be ok because conversion would be invoked in this case
return false;
}
boolean disposed = false;
for (SNode node : selectedNodes) {
if (!SNodeUtil.isAccessible(node, context.getRepository())) {
disposed = true;
break;
}
}
boolean canPasteWithRemove = !disposed && canPasteViaNodePasterWithRemove(selectedNodes, pasteNodes);
if (selection instanceof SingularSelection &&
(selection instanceof EditorCellLabelSelection && !isCompletelySelected((EditorCellLabelSelection) selection) ||
(selection instanceof EditorCellSelection && !canPasteWithRemove))) {
EditorCell selectedCell = getCellToPasteTo(context.getSelectedCell());
if (selectedCell == null) {
return false;
}
SNode selectedNode = selectedCell.getSNode();
if (selectedNode == null || !(SNodeUtil.isAccessible(selectedNode, context.getRepository()))) {
return false;
}
return canPasteViaNodePaster(selectedCell, pasteNodes);
} else if ((selection instanceof MultipleSelection || selection instanceof EditorCellSelection) && canPasteWithRemove) {
return true;
}
return false;
}
private boolean isCompletelySelected(EditorCellLabelSelection labelSelection) {
int textLength = labelSelection.getEditorCellLabel().getText().length();
return labelSelection.getSelectionStart() == 0 && labelSelection.getSelectionEnd() == textLength && textLength > 0;
}
private boolean canPasteViaNodePaster(EditorCell selectedCell, List<SNode> pasteNodes) {
if (!new NodePaster(pasteNodes).canPaste(selectedCell)) {
LOG.debug("Couldn't paste node here");
return false;
}
return true;
}
private boolean canPasteViaNodePasterWithRemove(List<SNode> pasteTargets, List<SNode> pasteNodes) {
if (!new NodePaster(pasteNodes).canPasteWithRemove(pasteTargets)) {
LOG.debug("Couldn't paste node here");
return false;
}
return true;
}
@Override
public void execute(final EditorContext context) {
LOG.assertLog(context.getRepository().getModelAccess().isCommandAction(), "This action must be performed in command");
final EditorComponent editorComponent = (EditorComponent) context.getEditorComponent();
final Selection selection = editorComponent.getSelectionManager().getSelection();
final List<SNode> selectedNodes = selection.getSelectedNodes();
//this is used in case node is in repo to pass it into invokeLater
final List<SNodeReference> selectedReferences = new ArrayList<>();
for (SNode node : selectedNodes) {
selectedReferences.add(node.getReference());
}
final CellInfo pasteTargetCellInfo;
final SNode cellNodeSelected;
final SNodeReference selectedCellReference;
if (selection instanceof SingularSelection) {
EditorCell pasteTargetCell = getCellToPasteTo(context.getSelectedCell());
if (pasteTargetCell == null) {
return;
}
pasteTargetCellInfo = pasteTargetCell.getCellInfo();
cellNodeSelected = pasteTargetCell.getSNode();
selectedCellReference = cellNodeSelected.getReference();
} else {
pasteTargetCellInfo = null;
cellNodeSelected = null;
selectedCellReference = null;
}
final SModel modelToPaste = selectedNodes.get(0).getModel();
// sometimes model is not in repository (paste in merge dialog)
final boolean inRepository = modelToPaste.getReference().resolve(context.getRepository()) != null;
// FIXME relationship between Project and Editor needs attention, it's bad to extract
final Project mpsProject = ProjectHelper.getProject(context.getRepository());
if (mpsProject == null) {
LOG.warning("Paste needs a project to show a dialog for additional imports");
return;
}
final PasteNodeData pasteNodeData = CopyPasteUtil.getPasteNodeDataFromClipboard(modelToPaste);
ApplicationManager.getApplication().invokeLater(() -> {
final Runnable addImportsRunnable = CopyPasteUtil.addImportsWithDialog(pasteNodeData, modelToPaste, mpsProject);
context.getRepository().getModelAccess().executeCommand(new EditorCommand(context) {
@Override
public void doExecute() {
if (addImportsRunnable != null) {
addImportsRunnable.run();
}
List<SNode> pasteNodes = pasteNodeData.getNodes();
List<SNode> currentSelectedNodes;
if (!inRepository) {
currentSelectedNodes = selectedNodes;
} else {
currentSelectedNodes = new ArrayList<>();
for (SNodeReference ref : selectedReferences) {
currentSelectedNodes.add(ref.resolve(context.getRepository()));
}
}
NodePaster nodePaster = new NodePaster(pasteNodes);
boolean disposed = CellAction_PasteNode.this.checkDisposedSelectedNodes(context.getRepository(), currentSelectedNodes, selectedReferences);
boolean canPasteWithRemove = !disposed && nodePaster.canPasteWithRemove(currentSelectedNodes);
if (selection instanceof SingularSelection &&
(selection instanceof EditorCellLabelSelection && !CellAction_PasteNode.this.isCompletelySelected((EditorCellLabelSelection) selection) ||
(selection instanceof EditorCellSelection && !canPasteWithRemove))) {
EditorCell selectedCell = pasteTargetCellInfo.findCell(editorComponent);
assert selectedCell != null;
if (CellAction_PasteNode.this.canPasteBefore(selectedCell, pasteNodes)) {
SNode selectedNode = inRepository ? selectedCellReference.resolve(context.getRepository()) : cellNodeSelected;
if (CellAction_PasteNode.this.checkDisposed(context.getRepository(), selectedCellReference, cellNodeSelected)) {
return;
}
new NodePaster(pasteNodes).pasteRelative(selectedNode, PastePlaceHint.BEFORE_ANCHOR);
} else {
new NodePaster(pasteNodes).paste(selectedCell);
}
} else if ((selection instanceof MultipleSelection || selection instanceof EditorCellSelection) && canPasteWithRemove) {
nodePaster.pasteWithRemove(currentSelectedNodes);
} else {
return;
}
Set<SReference> requireResolveReferences = new HashSet<>();
for (SReference ref : pasteNodeData.getRequireResolveReferences()) {
//ref can be detached from modeltoPaste while using copy/paste handlers
if (ref.getSourceNode() == null || ref.getSourceNode().getModel() == null) {
continue;
}
requireResolveReferences.add(ref);
}
ResolverComponent.getInstance().resolveScopesOnly(requireResolveReferences, context.getRepository());
// set selection
editorComponent.getUpdater().flushModelEvents();
SNode lastNode = pasteNodes.get(pasteNodes.size() - 1);
editorComponent.getSelectionManager().setSelection(lastNode, SelectionManager.LAST_CELL, -1);
}
});
}, ModalityState.current());
}
private boolean checkDisposedSelectedNodes(SRepository repository, List<SNode> currentSelectedNodes, List<SNodeReference> selectedReferences) {
Iterator<SNodeReference> referenceIterator = selectedReferences.iterator();
for (SNode node : currentSelectedNodes) {
SNodeReference reference = referenceIterator.next();
if (checkDisposed(repository, reference, node)) {
return true;
}
}
return false;
}
private boolean checkDisposed(SRepository repository, SNodeReference currentSelectedReference, SNode selectedNode) {
if (!SNodeUtil.isAccessible(selectedNode, repository)) {
LOG.error(String.format("Selected node is disposed: node = %s ; node pointer = (%s)", selectedNode, currentSelectedReference));
return true;
}
return false;
}
private boolean canPasteBefore(EditorCell selectedCell, List<SNode> pasteNodes) {
if (!GeometryUtil.isFirstPositionInBigCell(selectedCell)) {
return false;
}
SNode anchor = selectedCell.getSNode();
if (anchor.getParent() == null) {
return false;
}
final String role = anchor.getContainmentLink() != null ? anchor.getContainmentLink().getName() : null;
NodeAndRole nodeAndRole = new NodePaster(pasteNodes).getActualAnchorNode(anchor, role, false);
if (nodeAndRole == null) {
return false;
}
EditorCell targetCell = selectedCell.getEditorComponent().findNodeCell(nodeAndRole.myNode);
return targetCell != null && isFirstSelectableInTarget(selectedCell, targetCell) && new NodePaster(pasteNodes).canPasteRelative(nodeAndRole.myNode);
}
private boolean isFirstSelectableInTarget(@NotNull EditorCell selectedCell, @NotNull EditorCell targetCell) {
if (!CellTraversalUtil.getParents(selectedCell, true).contains(targetCell)) {
return false;
}
EditorCell prevSelectable = CellTraversalUtil.getPrevLeaf(selectedCell, CellConditions.SELECTABLE);
return prevSelectable == null || !CellTraversalUtil.getParents(prevSelectable, false).contains(targetCell);
}
private EditorCell getCellToPasteTo(EditorCell cell) {
if (cell == null) {
return null;
}
if (GeometryUtil.isLastPositionInBigCell(cell)) {
return cell;
}
if (cell instanceof EditorCell_Label && cell.getRole() == null) {
EditorCell result = new ChildrenCollectionFinder(cell, true, false).find();
if (result != null) {
return result;
}
result = new ChildrenCollectionFinder(cell, false, false).find();
if (result != null) {
if (result instanceof EditorCell_Collection) {
return ((EditorCell_Collection) result).lastCell();
}
return result;
}
}
return cell;
}
}