/*
* 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;
import jetbrains.mps.editor.runtime.cells.AbstractCellAction;
import jetbrains.mps.nodeEditor.actions.CursorPositionTracker;
import jetbrains.mps.nodeEditor.cells.CellFinderUtil;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.nodeEditor.cells.GeometryUtil;
import jetbrains.mps.nodeEditor.selection.NodeRangeSelection;
import jetbrains.mps.openapi.editor.EditorComponent;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.CellConditions;
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.Selection;
import jetbrains.mps.openapi.editor.selection.SelectionManager;
import jetbrains.mps.openapi.editor.selection.SingularSelection;
import jetbrains.mps.openapi.editor.selection.SingularSelection.SideSelectDirection;
import jetbrains.mps.smodel.SNodeLegacy;
import jetbrains.mps.smodel.SNodeUtil;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.util.Condition;
import java.awt.Rectangle;
import java.util.List;
@SuppressWarnings("WeakerAccess")
public class NodeEditorActions {
public abstract static class NavigationAction extends AbstractCellAction {
public NavigationAction() {
super(false);
}
}
abstract static class HorizontalNavigationAction extends NavigationAction {
public EditorCell getNextAlignedLeaf(EditorCell cell) {
return CellTraversalUtil.getNextLeaf(cell,
c -> c.isSelectable() && !GeometryUtil.isAbove(cell, c) && !GeometryUtil.isAbove(c, cell) && GeometryUtil.isLeftToRight(cell, c));
}
public EditorCell getPrevAlignedLeaf(EditorCell cell) {
return CellTraversalUtil.getPrevLeaf(cell,
c -> c.isSelectable() && !GeometryUtil.isAbove(cell, c) && !GeometryUtil.isAbove(c, cell) && GeometryUtil.isLeftToRight(c, cell));
}
}
public static class MoveLocal extends HorizontalNavigationAction {
private boolean myHome;
public MoveLocal(boolean home) {
myHome = home;
}
@Override
public boolean canExecute(EditorContext context) {
return findTarget(context.getEditorComponent().getSelectionManager()) != null;
}
@Override
public void execute(EditorContext context) {
SelectionManager selectionManager = context.getEditorComponent().getSelectionManager();
EditorCell cell = findTarget(selectionManager);
selectionManager.setSelection(cell);
if (cell == null) {
return;
}
if (myHome) {
cell.home();
} else {
cell.end();
}
}
private EditorCell findTarget(SelectionManager selectionManager) {
Selection selection = selectionManager.getSelection();
if (selection == null) {
return null;
}
List<EditorCell> selectedCells = selection.getSelectedCells();
EditorCell cell = myHome ? selectedCells.get(0) : selectedCells.get(selectedCells.size() - 1);
EditorCell leaf = myHome ? getPrevAlignedLeaf(cell) : getNextAlignedLeaf(cell);
if (leaf != null) {
return leaf;
}
return myHome ? CellTraversalUtil.getPrevLeaf(cell, CellConditions.SELECTABLE) : CellTraversalUtil.getNextLeaf(cell, CellConditions.SELECTABLE);
}
}
public static class MoveLeft extends HorizontalNavigationAction {
@Override
public boolean canExecute(EditorContext context) {
EditorCell selection = getDeepestSelectedCell(context);
return selection != null && findTarget(selection) != null;
}
@Override
public void execute(EditorContext context) {
EditorCell selection = getDeepestSelectedCell(context);
EditorCell target = findTarget(selection);
context.getEditorComponent().changeSelection(target);
if (target instanceof EditorCell_Label) {
EditorCell_Label label = (EditorCell_Label) target;
label.end();
}
}
private EditorCell getDeepestSelectedCell(EditorContext context) {
Selection deepestSelection = context.getEditorComponent().getSelectionManager().getDeepestSelection();
if (deepestSelection instanceof SingularSelection) {
return ((SingularSelection) deepestSelection).getEditorCell();
} else if (deepestSelection instanceof NodeRangeSelection) {
return ((NodeRangeSelection) deepestSelection).getFirstCell();
}
return null;
}
private EditorCell findTarget(EditorCell cell) {
EditorCell toLeft = getPrevAlignedLeaf(cell);
if (toLeft != null) {
return toLeft;
}
return CellTraversalUtil.getPrevLeaf(cell, CellConditions.SELECTABLE);
}
}
public static class MoveToRoot extends NavigationAction {
private boolean myHome;
public MoveToRoot(boolean home) {
myHome = home;
}
@Override
public boolean canExecute(EditorContext context) {
return findTarget(context.getEditorComponent().getSelectionManager()) != null;
}
@Override
public void execute(EditorContext context) {
SelectionManager selectionManager = context.getEditorComponent().getSelectionManager();
selectionManager.setSelection(findTarget(selectionManager));
selectionManager.getSelection().ensureVisible();
}
private EditorCell findTarget(SelectionManager selectionManager) {
Selection selection = selectionManager.getSelection();
if (selection == null) {
return null;
}
EditorCell cell = selection.getSelectedCells().get(0);
while (cell.getParent() != null) {
cell = cell.getParent();
}
if (cell instanceof EditorCell_Collection) {
//TODO remove this cast
return myHome ? CellFinderUtil.findFirstSelectableLeaf(cell) : CellFinderUtil.findLastSelectableLeaf(cell);
}
return cell;
}
}
public static class MoveHome extends HorizontalNavigationAction {
@Override
public boolean canExecute(EditorContext context) {
return findTarget(context.getEditorComponent()) != null;
}
@Override
public void execute(EditorContext context) {
EditorComponent editorComponent = context.getEditorComponent();
EditorCell target = findTarget(editorComponent);
if (target instanceof EditorCell_Label) {
EditorCell_Label label = (EditorCell_Label) target;
label.home();
}
editorComponent.changeSelection(target);
}
private EditorCell findTarget(EditorComponent editorComponent) {
Selection selection = editorComponent.getSelectionManager().getSelection();
if (selection == null) {
return null;
}
List<EditorCell> selectedCells = selection.getSelectedCells();
EditorCell cell = selectedCells.get(0);
while (getPrevAlignedLeaf(cell) != null) {
cell = getPrevAlignedLeaf(cell);
}
if (cell != selectedCells.get(0)) {
// another cell was found to the left from the current selection
return cell;
} else {
// there is no cells to the left from selected one.
// selecting first cell (important if currently selected cell is collection)
return CellTraversalUtil.getFirstLeaf(cell);
}
}
}
public static class MoveEnd extends HorizontalNavigationAction {
@Override
public boolean canExecute(EditorContext context) {
return findTarget(context.getEditorComponent()) != null;
}
@Override
public void execute(EditorContext context) {
EditorComponent editorComponent = context.getEditorComponent();
EditorCell target = findTarget(editorComponent);
if (target instanceof EditorCell_Label) {
EditorCell_Label label = (EditorCell_Label) target;
label.end();
}
editorComponent.changeSelection(target);
}
private EditorCell findTarget(EditorComponent editorComponent) {
Selection selection = editorComponent.getSelectionManager().getSelection();
if (selection == null) {
return null;
}
List<EditorCell> selectedCells = selection.getSelectedCells();
EditorCell cell = selectedCells.get(selectedCells.size() - 1);
while (getNextAlignedLeaf(cell) != null) {
cell = getNextAlignedLeaf(cell);
}
if (cell != selectedCells.get(0)) {
// another cell was found to the right from the current selection
return cell;
} else {
// there is no cells to the right from selected one.
// selecting last cell (important if currently selected cell is collection)
return CellTraversalUtil.getLastLeaf(cell);
}
}
}
public static class MoveRight extends HorizontalNavigationAction {
@Override
public boolean canExecute(EditorContext context) {
EditorCell selection = getDeepestSelectedCell(context);
return selection != null && findTarget(selection) != null;
}
@Override
public void execute(EditorContext context) {
EditorCell selection = getDeepestSelectedCell(context);
EditorCell target = findTarget(selection);
context.getEditorComponent().changeSelection(target);
if (target instanceof EditorCell_Label) {
EditorCell_Label label = (EditorCell_Label) target;
label.home();
}
}
private EditorCell getDeepestSelectedCell(EditorContext context) {
Selection deepestSelection = context.getEditorComponent().getSelectionManager().getDeepestSelection();
if (deepestSelection instanceof SingularSelection) {
return ((SingularSelection) deepestSelection).getEditorCell();
} else if (deepestSelection instanceof NodeRangeSelection) {
return ((NodeRangeSelection) deepestSelection).getLastCell();
}
return null;
}
private EditorCell findTarget(EditorCell cell) {
EditorCell toRight = getNextAlignedLeaf(cell);
if (toRight != null) {
return toRight;
}
return CellTraversalUtil.getNextLeaf(cell, CellConditions.SELECTABLE);
}
}
public static class MoveUp extends NavigationAction {
private final CursorPositionTracker myPositionTracker;
public MoveUp(CursorPositionTracker positionTracker) {
myPositionTracker = positionTracker;
}
@Override
public boolean canExecute(EditorContext context) {
EditorCell selectedCell = getDeepestSelectedCell(context);
return selectedCell != null && selectedCell.getParent() != null && findTarget(selectedCell, selectedCell.getCaretX()) != null;
}
@Override
public void execute(EditorContext context) {
EditorCell selectedCell = getDeepestSelectedCell(context);
int caretX = myPositionTracker.hasPosition() ? myPositionTracker.getPosition() : selectedCell.getCaretX();
EditorCell target = findTarget(selectedCell, caretX);
target.setCaretX(caretX);
context.getEditorComponent().changeSelection(target);
myPositionTracker.savePosition(caretX);
}
private EditorCell getDeepestSelectedCell(EditorContext context) {
Selection deepestSelection = context.getEditorComponent().getSelectionManager().getDeepestSelection();
if (deepestSelection instanceof SingularSelection) {
return ((SingularSelection) deepestSelection).getEditorCell();
} else if (deepestSelection instanceof NodeRangeSelection) {
return ((NodeRangeSelection) deepestSelection).getFirstCell();
}
return null;
}
private EditorCell findTarget(EditorCell cell, int baseX) {
EditorCell bestMatch = null;
Condition<EditorCell> condition = c -> c.isSelectable() && GeometryUtil.isAbove(c, cell);
for (EditorCell nextCandidate = CellTraversalUtil.getPrevLeaf(cell, condition); nextCandidate != null;
nextCandidate = CellTraversalUtil.getPrevLeaf(nextCandidate, condition)) {
if (bestMatch != null && GeometryUtil.isAbove(nextCandidate, bestMatch)) {
break;
}
if (bestMatch == null || GeometryUtil.getHorizontalDistance(bestMatch, baseX) > GeometryUtil.getHorizontalDistance(nextCandidate, baseX)) {
bestMatch = nextCandidate;
}
}
return bestMatch;
}
}
public static class MoveDown extends NavigationAction {
private final CursorPositionTracker myPositionTracker;
public MoveDown(CursorPositionTracker positionTracker) {
myPositionTracker = positionTracker;
}
@Override
public boolean canExecute(EditorContext context) {
EditorCell selectedCell = getDeepestSelectedCell(context);
return selectedCell != null && findTarget(selectedCell, selectedCell.getCaretX()) != null;
}
@Override
public void execute(EditorContext context) {
EditorCell selectedCell = getDeepestSelectedCell(context);
int caretX = myPositionTracker.hasPosition() ? myPositionTracker.getPosition() : selectedCell.getCaretX();
EditorCell target = findTarget(selectedCell, caretX);
target.setCaretX(caretX);
context.getEditorComponent().changeSelection(target);
myPositionTracker.savePosition(caretX);
}
private EditorCell getDeepestSelectedCell(EditorContext context) {
Selection deepestSelection = context.getSelectionManager().getDeepestSelection();
if (deepestSelection instanceof SingularSelection) {
return ((SingularSelection) deepestSelection).getEditorCell();
} else if (deepestSelection instanceof NodeRangeSelection) {
return ((NodeRangeSelection) deepestSelection).getLastCell();
}
return null;
}
private EditorCell findTarget(EditorCell cell, int caretX) {
EditorCell bestMatch = null;
Condition<EditorCell> condition = c -> c.isSelectable() && GeometryUtil.isAbove(cell, c);
for (EditorCell nextCandidate = CellTraversalUtil.getNextLeaf(cell, condition); nextCandidate != null;
nextCandidate = CellTraversalUtil.getNextLeaf(nextCandidate, condition)) {
if (bestMatch != null && GeometryUtil.isAbove(bestMatch, nextCandidate)) {
break;
}
if (bestMatch == null || GeometryUtil.getHorizontalDistance(bestMatch, caretX) > GeometryUtil.getHorizontalDistance(nextCandidate, caretX)) {
bestMatch = nextCandidate;
}
}
return bestMatch;
}
}
public static class MoveNext extends NavigationAction {
@Override
public boolean canExecute(EditorContext context) {
EditorCell selection = ((jetbrains.mps.nodeEditor.EditorComponent) context.getEditorComponent()).getSelectedCell();
return selection != null && CellTraversalUtil.getNextLeaf(selection, CellConditions.EDITABLE) != null;
}
@Override
public void execute(EditorContext context) {
EditorCell selection = ((jetbrains.mps.nodeEditor.EditorComponent) context.getEditorComponent()).getSelectedCell();
if (selection == null) {
return;
}
context.getEditorComponent().changeSelection(CellTraversalUtil.getNextLeaf(selection, CellConditions.EDITABLE));
}
}
public static class MovePrev extends NavigationAction {
@Override
public boolean canExecute(EditorContext context) {
EditorCell selection = ((jetbrains.mps.nodeEditor.EditorComponent) context.getEditorComponent()).getSelectedCell();
return selection != null && CellTraversalUtil.getPrevLeaf(selection, CellConditions.EDITABLE) != null;
}
@Override
public void execute(EditorContext context) {
EditorCell selection = ((jetbrains.mps.nodeEditor.EditorComponent) context.getEditorComponent()).getSelectedCell();
if (selection == null) {
return;
}
context.getEditorComponent().changeSelection(CellTraversalUtil.getPrevLeaf(selection, CellConditions.EDITABLE));
}
}
private static void navigatePage(EditorContext context, boolean isDown) {
jetbrains.mps.nodeEditor.EditorComponent editor = (jetbrains.mps.nodeEditor.EditorComponent) context.getEditorComponent();
EditorCell selection = editor.getSelectedCell();
if (selection == null) {
return;
}
Rectangle rect = editor.getVisibleRect();
int height = (int) rect.getHeight();
height = isDown ? height : -height;
int caretX = selection.getCaretX();
int y = selection.getY() + (selection.getHeight() / 2);
int newY = y + height;
EditorCell target = editor.findCellWeak(caretX, newY);
if (target == null) {
target = isDown ? CellFinderUtil.findLastSelectableLeaf(editor.myRootCell) : CellFinderUtil.findFirstSelectableLeaf(editor.myRootCell);
editor.changeSelection(target);
} else {
target.setCaretX(caretX);
editor.changeSelection(target);
}
}
public static class MovePageUp extends NavigationAction {
@Override
public boolean canExecute(EditorContext context) {
return true;
}
@Override
public void execute(EditorContext context) {
navigatePage(context, true);
}
}
public static class MovePageDown extends NavigationAction {
@Override
public boolean canExecute(EditorContext context) {
return true;
}
@Override
public void execute(EditorContext context) {
navigatePage(context, false);
}
}
public static class SelectUp extends NavigationAction {
@Override
public boolean canExecute(EditorContext context) {
return findTarget(context.getEditorComponent().getSelectionManager()) != null;
}
@Override
public void execute(EditorContext context) {
SelectionManager selectionManager = context.getEditorComponent().getSelectionManager();
EditorCell cell = findTarget(selectionManager);
selectionManager.pushSelection(selectionManager.createSelection(cell));
if (cell instanceof EditorCell_Label) {
((EditorCell_Label) cell).selectWordOrAll();
}
}
private EditorCell findTarget(SelectionManager selectionManager) {
Selection selection = selectionManager.getSelection();
if (selection == null) {
return null;
}
EditorCell cell = selection.getSelectedCells().get(0);
if (cell instanceof EditorCell_Label && !((EditorCell_Label) cell).isEverythingSelected()) {
return cell;
}
if (cell.getParent() == null) {
return null;
}
while (cell.getParent() != null && cell.getParent().isTransparentCollection()) {
cell = cell.getParent();
}
EditorCell_Collection parent = cell.getParent();
while (parent != null) {
if (parent.isSelectable()) {
while (parent.getParent() != null && parent.getParent().isTransparentCollection() && parent.getParent().isSelectable()) {
parent = parent.getParent();
}
return parent;
}
parent = parent.getParent();
}
return null;
}
}
public static class SelectDown extends NavigationAction {
@Override
public boolean canExecute(EditorContext context) {
return context.getEditorComponent().getSelectionManager().getSelectionStackSize() > 1;
}
@Override
public void execute(EditorContext context) {
context.getEditorComponent().getSelectionManager().popSelection();
}
}
public static class SideSelect extends NavigationAction {
private CellSide mySide;
protected SideSelect(CellSide side) {
mySide = side;
}
private EditorCell getNextLeaf(EditorCell current) {
if (mySide == CellSide.LEFT) {
return CellTraversalUtil.getPrevLeaf(current, jetbrains.mps.openapi.editor.cells.CellConditions.SELECTABLE);
} else {
return CellTraversalUtil.getNextLeaf(current, jetbrains.mps.openapi.editor.cells.CellConditions.SELECTABLE);
}
}
@Override
public boolean canExecute(EditorContext context) {
SelectionManager selectionManager = context.getEditorComponent().getSelectionManager();
Selection selection = selectionManager.getSelection();
if (selection instanceof SingularSelection) {
SingularSelection singularSelection = (SingularSelection) selection;
if (!expandSelection(singularSelection) && selectionManager.getSelectionStackSize() > 1) {
return true;
}
EditorCell selected = singularSelection.getEditorCell();
EditorCell nextLeaf = getNextLeaf(selected);
return nextLeaf != null && getCommonSelectableAncestor(selected, nextLeaf) != null;
}
return false;
}
@Override
public void execute(EditorContext context) {
SelectionManager selectionManager = context.getEditorComponent().getSelectionManager();
SingularSelection selection = (SingularSelection) selectionManager.getSelection();
if (!expandSelection(selection) && selectionManager.getSelectionStackSize() > 1) {
selectionManager.popSelection();
return;
}
EditorCell selected = selection.getEditorCell();
EditorCell nextLeaf = getNextLeaf(selected);
EditorCell cellToSelect = getCommonSelectableAncestor(selected, nextLeaf);
Selection newSelection = selectionManager.createSelection(cellToSelect);
if (newSelection instanceof SingularSelection) {
((SingularSelection) newSelection).setSideSelectDirection(mySide == CellSide.LEFT ? SideSelectDirection.LEFT : SideSelectDirection.RIGHT);
}
selectionManager.pushSelection(newSelection);
}
private boolean expandSelection(SingularSelection selection) {
switch (selection.getSideSelectDirection()) {
case LEFT:
return mySide == CellSide.LEFT;
case RIGHT:
return mySide == CellSide.RIGHT;
}
return true;
}
private EditorCell getCommonSelectableAncestor(EditorCell first, EditorCell... cells) {
EditorCell_Collection result = first instanceof EditorCell_Collection ? (EditorCell_Collection) first : first.getParent();
while (result != null) {
if (result.isSelectable()) {
boolean common = true;
for (EditorCell cell : cells) {
if (!result.isAncestorOf(cell) && result != cell) {
common = false;
break;
}
}
if (common) {
return result;
}
}
result = result.getParent();
}
return null;
}
}
public static class EnlargeSelection extends NavigationAction {
private boolean myUp;
public EnlargeSelection(boolean up) {
myUp = up;
}
@Override
public boolean canExecute(EditorContext context) {
// TODO: this check should be reallocated into Selection.canExecuteAction() method once it created
Selection selection = context.getEditorComponent().getSelectionManager().getSelection();
if (selection instanceof NodeRangeSelection) {
return true;
}
if (selection instanceof SingularSelection) {
EditorCell editorCell = ((SingularSelection) selection).getEditorCell();
return editorCell.getSNode().getParent() != null;
}
return false;
}
@Override
public void execute(final EditorContext context) {
EditorComponent editorComponent = context.getEditorComponent();
SelectionManager selectionManager = editorComponent.getSelectionManager();
Selection selection = selectionManager.getSelection();
if (selection instanceof SingularSelection) {
EditorCell selectedCell = ((SingularSelection) selection).getEditorCell();
SNode selectedNode = selectedCell.getSNode();
SNode topMostNodeInSingularContainment = findTopMostNodeWithSingularContainment(selectedNode);
if (topMostNodeInSingularContainment != selectedNode) {
EditorCell nodeCell = editorComponent.findNodeCell(topMostNodeInSingularContainment);
if (nodeCell != null) {
((jetbrains.mps.nodeEditor.EditorComponent) editorComponent).pushSelection(nodeCell);
editorComponent.scrollToCell(nodeCell);
}
} else {
Selection newSelection = selectionManager.createRangeSelection(selectedNode, selectedNode);
if (newSelection instanceof NodeRangeSelection && selectedCell.isBig()) {
newSelection = ((NodeRangeSelection) newSelection).enlargeSelection(myUp);
}
if (newSelection != null) {
selectionManager.pushSelection(newSelection);
newSelection.ensureVisible();
}
}
} else if (selection instanceof NodeRangeSelection) {
Selection newSelection = ((NodeRangeSelection) selection).enlargeSelection(myUp);
if (newSelection != null) {
selectionManager.pushSelection(newSelection);
newSelection.ensureVisible();
}
}
}
private SNode findTopMostNodeWithSingularContainment(SNode childNode) {
while (childNode.getParent() != null && SNodeUtil.getLinkDeclaration_IsSingular(new SNodeLegacy(childNode).getRoleLink())) {
childNode = childNode.getParent();
}
return childNode;
}
}
public static class Complete extends AbstractCellAction {
@Override
public boolean canExecute(EditorContext context) {
EditorCell selection = context.getSelectedCell();
return selection != null && selection.getSubstituteInfo() != null;
}
@Override
public void execute(EditorContext context) {
EditorCell selection = context.getSelectedCell();
((jetbrains.mps.nodeEditor.EditorComponent) context.getEditorComponent()).activateNodeSubstituteChooser(selection,
((selection instanceof EditorCell_Label) && ((EditorCell_Label) selection).isEverythingSelected()), false);
}
}
public static class CompleteSmart extends AbstractCellAction {
@Override
public boolean canExecute(EditorContext context) {
EditorCell selection = context.getSelectedCell();
return selection != null && selection.getSubstituteInfo() != null;
}
@Override
public void execute(EditorContext context) {
EditorCell selection = context.getSelectedCell();
((jetbrains.mps.nodeEditor.EditorComponent) context.getEditorComponent()).activateNodeSubstituteChooser(selection,
((selection instanceof EditorCell_Label) && ((EditorCell_Label) selection).isEverythingSelected()), true);
}
}
public static class ShowMessage extends AbstractCellAction {
@Override
public void execute(EditorContext context) {
((jetbrains.mps.nodeEditor.EditorComponent) context.getEditorComponent()).showMessageTooltip();
}
}
}