/*
* Copyright 2003-2011 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.cells;
import com.intellij.openapi.command.CommandProcessor;
import jetbrains.mps.editor.runtime.TextBuilderImpl;
import jetbrains.mps.editor.runtime.cells.AbstractCellAction;
import jetbrains.mps.editor.runtime.cells.CaretState;
import jetbrains.mps.editor.runtime.commands.EditorComputable;
import jetbrains.mps.editor.runtime.style.Padding;
import jetbrains.mps.editor.runtime.style.StyleAttributes;
import jetbrains.mps.editor.runtime.style.StyleAttributesUtil;
import jetbrains.mps.ide.datatransfer.CopyPasteUtil;
import jetbrains.mps.ide.datatransfer.TextPasteUtil;
import jetbrains.mps.nodeEditor.CellSide;
import jetbrains.mps.nodeEditor.IntelligentInputUtil;
import jetbrains.mps.nodeEditor.cellMenu.NodeSubstitutePatternEditor;
import jetbrains.mps.nodeEditor.selection.EditorCellLabelSelection;
import jetbrains.mps.nodeEditor.ui.InputMethodListenerImpl;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.TextBuilder;
import jetbrains.mps.openapi.editor.cells.CellAction;
import jetbrains.mps.openapi.editor.cells.CellActionType;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.SubstituteInfo;
import jetbrains.mps.openapi.editor.cells.optional.WithCaret;
import jetbrains.mps.openapi.editor.selection.MultipleSelection;
import jetbrains.mps.openapi.editor.selection.SelectionManager;
import jetbrains.mps.smodel.SNodeUndoableAction;
import jetbrains.mps.smodel.UndoHelper;
import jetbrains.mps.smodel.UndoRunnable;
import jetbrains.mps.util.EqualUtil;
import jetbrains.mps.util.NameUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.module.ModelAccess;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.InputMethodEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
public abstract class EditorCell_Label extends EditorCell_Basic implements jetbrains.mps.openapi.editor.cells.EditorCell_Label, WithCaret {
protected boolean myNoTextSet;
protected TextLine myTextLine;
protected TextLine myNullTextLine;
protected CaretState myCaretState = new CaretState();
protected EditorCell_Label(@NotNull jetbrains.mps.openapi.editor.EditorContext editorContext, SNode node, String text) {
super(editorContext, node);
myTextLine = new TextLine("", getStyle(), false);
myNullTextLine = new TextLine("", getStyle(), true);
myTextLine.setCaretEnabled(true);
myNullTextLine.setCaretEnabled(true);
setText(text);
setAction(CellActionType.LEFT, new MoveLeft(false));
setAction(CellActionType.RIGHT, new MoveRight(false));
setAction(CellActionType.LOCAL_HOME, new LocalHome(false));
setAction(CellActionType.LOCAL_END, new LocalEnd(false));
setAction(CellActionType.SELECT_RIGHT, new MoveRight(true));
setAction(CellActionType.SELECT_LEFT, new MoveLeft(true));
setAction(CellActionType.SELECT_HOME, new SelectHome());
setAction(CellActionType.SELECT_END, new SelectEnd());
setAction(CellActionType.SELECT_LOCAL_HOME, new LocalHome(true));
setAction(CellActionType.SELECT_LOCAL_END, new LocalEnd(true));
setAction(CellActionType.COPY, new CopyLabelText());
setAction(CellActionType.PASTE, new PasteIntoLabelText());
setAction(CellActionType.CUT, new CutLabelText());
setAction(CellActionType.CLEAR_SELECTION, new ClearSelection());
}
@Override
public boolean isFirstPositionInBigCell() {
return CellTraversalUtil.getFirstLeaf(CellTraversalUtil.getContainingBigCell(this)) == this && isFirstCaretPosition();
}
@Override
public boolean isLastPositionInBigCell() {
return CellTraversalUtil.getLastLeaf(CellTraversalUtil.getContainingBigCell(this)) == this && isLastCaretPosition() &&
!getTextLine().hasNonTrivialSelection();
}
public boolean canPasteText() {
return true;
}
@Override
public void setSelected(boolean selected) {
super.setSelected(selected);
if (!selected && !getEditor().selectionStackContains(this)) {
myTextLine.resetSelection();
}
myCaretState.touch(selected);
}
@Override
public String getText() {
return myTextLine.getText();
}
public String getNullText() {
return myNullTextLine.getText();
}
public String getRenderedText() {
return getRenderedTextLine().getText();
}
public Font getFont() {
return getRenderedTextLine().getFont();
}
public void setTextColor(Color color) {
getStyle().set(StyleAttributes.TEXT_COLOR, color);
}
public void setNullTextColor(Color color) {
getStyle().set(StyleAttributes.NULL_TEXT_COLOR, color);
}
public void setTextBackgroundColor(Color color) {
getStyle().set(StyleAttributes.TEXT_BACKGROUND_COLOR, color);
}
public void setNullTextBackgroundColor(Color color) {
getStyle().set(StyleAttributes.NULL_TEXT_BACKGROUND_COLOR, color);
}
public void setSelectedTextBackgroundColor(Color color) {
getStyle().set(StyleAttributes.SELECTED_TEXT_BACKGROUND_COLOR, color);
}
public void setNullSelectedTextBackgroundColor(Color color) {
getStyle().set(StyleAttributes.NULL_SELECTED_TEXT_BACKGROUND_COLOR, color);
}
@Override
public int getCaretPosition() {
return myTextLine.getCaretPosition();
}
@Override
public void setCaretPosition(int position) {
setCaretPosition(position, false);
}
public void setCaretPositionIfPossible(int position) {
if (isCaretPositionAllowed(position)) {
setCaretPosition(position, false);
}
}
public void setCaretPosition(int position, boolean selection) {
assert isCaretPositionAllowed(position) : "Position " + position + " is not allowed for EditorCell_Label: \"" + myTextLine.getText() + "\"";
repaintCaret();
myTextLine.setCaretPosition(position, selection);
myCaretState.touch();
repaintCaret();
}
public boolean isCaretPositionAllowed(int position) {
if (!StyleAttributesUtil.isFirstPositionAllowed(getStyle()) && position == 0) {
return false;
}
if (!StyleAttributesUtil.isLastPositionAllowed(getStyle()) && position == myTextLine.getText().length()) {
return false;
}
return position >= 0 && position <= myTextLine.getText().length();
}
@Override
public void home() {
int textLength = getText().length();
if (StyleAttributesUtil.isFirstPositionAllowed(getStyle())) {
if (textLength > 0 || StyleAttributesUtil.isLastPositionAllowed(getStyle())) {
setCaretPosition(0);
}
} else {
if (textLength > 0 && (textLength > 1 || StyleAttributesUtil.isLastPositionAllowed(getStyle()))) {
setCaretPosition(1);
}
}
}
@Override
public void end() {
int textLength = getText().length();
if (StyleAttributesUtil.isLastPositionAllowed(getStyle())) {
if (textLength > 0 || StyleAttributesUtil.isFirstPositionAllowed(getStyle())) {
setCaretPosition(getText().length());
}
} else {
if (textLength > 0 && (textLength > 1 || StyleAttributesUtil.isFirstPositionAllowed(getStyle()))) {
setCaretPosition(getText().length() - 1);
}
}
}
@Override
public boolean isSelectable() {
if (!super.isSelectable()) {
return false;
}
if (getText() == null || getText().length() == 0) {
return StyleAttributesUtil.isFirstPositionAllowed(getStyle()) && StyleAttributesUtil.isLastPositionAllowed(getStyle());
}
if (getText().length() == 1) {
return StyleAttributesUtil.isFirstPositionAllowed(getStyle()) || StyleAttributesUtil.isLastPositionAllowed(getStyle());
}
return true;
}
@Override
public boolean isFirstCaretPosition() {
if (!StyleAttributesUtil.isFirstPositionAllowed(getStyle())) {
return getCaretPosition() == 1;
} else {
return getCaretPosition() == 0;
}
}
@Override
public boolean isLastCaretPosition() {
if (!StyleAttributesUtil.isLastPositionAllowed(getStyle())) {
return getCaretPosition() == getText().length() - 1;
} else {
return getCaretPosition() == getText().length();
}
}
@Override
public boolean isCaretEnabled() {
return myTextLine.isCaretEnabled();
}
@Override
public void setCaretEnabled(boolean enabled) {
myTextLine.setCaretEnabled(enabled);
}
public void setText(String text) {
myNoTextSet = (text == null || text.length() == 0);
myTextLine.setText(myNoTextSet ? null : text);
requestRelayout();
}
public void setDefaultText(String text) {
myNullTextLine.setText(text);
}
@Override
public int getEffectiveWidth() {
return getTextLineWidth();
}
@Override
public int getLeftInset() {
return getRenderedTextLine().getPaddingLeft() + myGapLeft;
}
@Override
public int getRightInset() {
return getRenderedTextLine().getPaddingRight() + myGapRight;
}
@Override
public int getTopInset() {
return getRenderedTextLine().getPaddingTop();
}
@Override
public int getBottomInset() {
return getRenderedTextLine().getPaddingBottom();
}
public int getTextLineWidth() {
int textLineWidth;
if (myNoTextSet && myTextLine.getText().length() == 0) {
textLineWidth = myNullTextLine.getEffectiveWidth();
} else {
textLineWidth = myTextLine.getEffectiveWidth();
}
if (isDrawBrackets()) {
textLineWidth += 2 * BRACKET_WIDTH;
}
return textLineWidth;
}
public boolean isEditable() {
return getStyle().get(StyleAttributes.EDITABLE);
}
public void setEditable(boolean editable) {
getStyle().set(StyleAttributes.EDITABLE, editable);
}
@Override
public void setErrorState(boolean errorState) {
super.setErrorState(errorState);
if (errorState) {
myTextLine.showErrorColor();
myNullTextLine.showErrorColor();
} else {
myTextLine.showTextColor();
myNullTextLine.showTextColor();
}
}
@Override
public void relayoutImpl() {
if (isPunctuationLayout()) {
getStyle().set(StyleAttributes.PADDING_LEFT, new Padding(0.0));
getStyle().set(StyleAttributes.PADDING_RIGHT, new Padding(0.0));
}
if (myNoTextSet && myTextLine.getText().length() == 0) {
myNullTextLine.relayout();
myHeight = myNullTextLine.getHeight();
myWidth = myNullTextLine.getWidth();
} else {
myTextLine.relayout();
myHeight = myTextLine.getHeight();
myWidth = myTextLine.getWidth();
}
}
@Override
public void switchCaretVisible() {
myCaretState.tick();
repaintCaret();
}
@Override
public void setCaretVisible(boolean visible) {
myCaretState.touch(visible);
repaintCaret();
}
private void repaintCaret() {
getRenderedTextLine().repaintCaret(getEditor(), getShiftX(), getY());
}
@Override
protected boolean isSelectionPainted() {
return isSelected() && getEditor().getSelectionManager().getSelection() instanceof MultipleSelection;
}
@Override
protected void paintContent(Graphics g, ParentSettings parentSettings) {
TextLine textLine = getRenderedTextLine();
boolean toShowCaret = toShowCaret();
boolean selected = isSelectionPaintedOnAncestor(parentSettings).isSelectionPainted();
textLine.setSelected(selected);
textLine.setShowCaret(toShowCaret);
Color cellFontColor = getEditor().getAdditionalCellFontColor(this);
textLine.paint(g, getShiftX(), myY, cellFontColor);
}
@Override
public void paintSelection(Graphics g, Color c, boolean drawBorder, ParentSettings parentSettings) {
if (!isSelectionPaintedOnAncestor(parentSettings).isSelectionPainted() && getEditor().getAdditionalCellFontColor(this) != null) {
/*
* Suppressing selection painting in case this cell is not actually selected and additionalCellFontColor() for it is not null.
* This will hide messages feedback if there is an AdditionalPainter instance (with specified cellFontColor) covering this cell.
* Probably it's good idea to use separate property (not cellFontColor) to determine if this AdditionalPainter is "hiding" messages feedback
* or simply let some additional painters paint background below and above editor messages.
*/
return;
}
super.paintSelection(g, c, drawBorder, parentSettings);
}
protected boolean toShowCaret() {
return myCaretState.isVisible() && isWithinSelection();
}
TextLine getTextLine() {
return myTextLine;
}
TextLine getNullTextLine() {
return myNullTextLine;
}
public TextLine getRenderedTextLine() {
TextLine textLine;
if (myNoTextSet && myTextLine.getText().length() == 0) {
textLine = myNullTextLine;
} else {
textLine = myTextLine;
}
return textLine;
}
@Override
public void setCaretX(int x) {
myTextLine.setCaretByXCoord(x - getShiftX());
makePositionValid();
}
@Override
public int getCaretX() {
return myTextLine.getCaretX(getShiftX());
}
private int getShiftX() {
return isDrawBrackets() ? myX + myGapLeft + BRACKET_WIDTH : myX + myGapLeft;
}
private void makePositionValid() {
if (myTextLine.getCaretPosition() == 0 && !StyleAttributesUtil.isFirstPositionAllowed(getStyle()) && isCaretPositionAllowed(1)) {
setCaretPosition(1);
}
if (myTextLine.getCaretPosition() == getText().length() && !StyleAttributesUtil.isLastPositionAllowed(getStyle()) &&
isCaretPositionAllowed(getText().length() - 1)) {
setCaretPosition(getText().length() - 1);
}
getEditor().getBracesHighlighter().updateBracesSelection(this);
}
@Override
public boolean processMousePressed(MouseEvent e) {
setCaretX(e.getX());
getEditor().repaint(this);
return true;
}
public void ensureCaretVisible() {
getEditor().scrollRectToVisible(new Rectangle(getCaretX() - 2 * myTextLine.charWidth(), myY, 4 * myTextLine.charWidth(), myHeight));
}
@Override
protected boolean doProcessKeyTyped(final KeyEvent keyEvent, final boolean allowErrors) {
if (!isTextTypedEvent(keyEvent)) {
return false;
}
int caretPosition = getCaretPosition();
CellSide side;
if (caretPosition == 0) {
side = CellSide.LEFT;
} else if (caretPosition == getRenderedText().length()) {
side = CellSide.RIGHT;
} else {
side = null;
}
ModelAccess modelAccess = getContext().getRepository().getModelAccess();
String text = String.valueOf(keyEvent.getKeyChar());
if (isEditable()) {
ModifyTextCommand keyTypedCommand = new ModifyTextCommand(text, allowErrors, side, getContext());
modelAccess.executeCommand(keyTypedCommand);
getEditor().relayout();
return keyTypedCommand.getResult();
} else if (side != null) {
String pattern = getUpdatedText(text);
if (!pattern.equals(getRenderedText())) {
return IntelligentInputUtil.processCell(this, getContext(), pattern, side);
}
}
return false;
}
@Override
public boolean processTextChanged(InputMethodEvent e) {
if (!isEditable()) {
return false;
}
ModelAccess modelAccess = getContext().getRepository().getModelAccess();
if (!myTextLine.hasNonTrivialSelection()) {
// selecting last symbol entered by user in order to replace if with the result fo input method processing
int caretPosition = getCaretPosition();
if (isCaretPositionAllowed(caretPosition - 1)) {
setCaretPosition(caretPosition - 1, true);
}
}
ModifyTextCommand keyTypedCommand = new ModifyTextCommand(InputMethodListenerImpl.getText(e), true, null, getContext());
modelAccess.executeCommand(keyTypedCommand);
if (keyTypedCommand.getResult()) {
getEditor().relayout();
}
return keyTypedCommand.getResult();
}
private void addChangeTextUndoableAction() {
UndoHelper.getInstance().addUndoableAction(new DummyUndoableAction(getSNode()));
}
@Override
public boolean executeTextAction(CellActionType type, boolean allowErrors) {
assert type == CellActionType.DELETE || type == CellActionType.BACKSPACE;
if (!isEditable()) {
return false;
}
// TODO: check if we need command here or we can execute command from UI action...
ProcessTextActionCommand textAction = new ProcessTextActionCommand(getContext(), type, allowErrors);
getContext().getRepository().getModelAccess().executeCommand(textAction);
return textAction.getResult();
}
/**
* @param replacingText the text, replacing selected fragment
* @return the string contained in myTextLine, updated by replacing selected text fragment with the replacingText parameter
*/
private String getUpdatedText(String replacingText) {
String currentText = myTextLine.getText();
int startSelection = myTextLine.getStartTextSelectionPosition();
int endSelection = myTextLine.getEndTextSelectionPosition();
return currentText.substring(0, startSelection) + replacingText + currentText.substring(endSelection);
}
private boolean canDeleteFrom(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
if (getText().length() == 0) {
return false;
}
if (!(cell instanceof EditorCell_Label)) {
return false;
}
EditorCell_Label label = (EditorCell_Label) cell;
return label.isEditable() && label.isSelectable();
}
private void deleteIfPossible(CellActionType actionType) {
assert CellActionType.DELETE == actionType || CellActionType.BACKSPACE == actionType;
if ("".equals(getText()) && getStyle().get(StyleAttributes.AUTO_DELETABLE)) {
// TODO: just use delete action (do not call getSNode().delete()) in the end if acton was not found or is not applicable
CellAction deleteAction = getEditorComponent().getActionHandler().getApplicableCellAction(this, actionType);
if (deleteAction != null) {
deleteAction.execute(getContext());
}
}
}
@Override
public String getSelectedText() {
return myTextLine.getTextuallySelectedText();
}
@Override
public int getSelectionStart() {
return myTextLine.getStartTextSelectionPosition();
}
@Override
public void setSelectionStart(int position) {
myTextLine.setStartTextSelectionPosition(position);
}
@Override
public int getSelectionEnd() {
return myTextLine.getEndTextSelectionPosition();
}
@Override
public void setSelectionEnd(int position) {
myTextLine.setEndTextSelectionPosition(position);
}
public void deleteSelection() {
String myText = myTextLine.getText();
int stSel = myTextLine.getStartTextSelectionPosition();
int endSel = myTextLine.getEndTextSelectionPosition();
changeText(myText.substring(0, stSel) + myText.substring(endSel));
myTextLine.setCaretPosition(stSel);
addChangeTextUndoableAction();
fireSelectionChanged();
ensureCaretVisible();
}
public void changeText(final String text) {
String oldText = getText();
setText(text);
updateVfsTimestamp(text, oldText);
}
private void updateVfsTimestamp(String text, String oldText) {
if (EqualUtil.equals(oldText, text) || isValidText(text)) {
return;
}
if (CommandProcessor.getInstance().getCurrentCommand() == null) {
return;
}
getEditorComponent().touch();
}
public void insertText(String text) {
int startSelectionPosition = myTextLine.getStartTextSelectionPosition();
int endSelectionPosition = myTextLine.getEndTextSelectionPosition();
if (startSelectionPosition >= endSelectionPosition) {
startSelectionPosition = myTextLine.getCaretPosition();
endSelectionPosition = myTextLine.getCaretPosition();
}
String oldText = getText();
changeText(oldText.substring(0, startSelectionPosition) + text + oldText.substring(endSelectionPosition));
myTextLine.setCaretPosition(startSelectionPosition);
myTextLine.setCaretPosition(startSelectionPosition + text.length(), true);
addChangeTextUndoableAction();
}
public boolean isValidText(String text) {
return true;
}
public void setUnderlined(boolean b) {
getStyle().set(StyleAttributes.UNDERLINED, b);
}
@Override
public int getAscent() {
return getRenderedTextLine().getAscent();
}
@Override
public int getDescent() {
return getRenderedTextLine().getDescent();
}
@Override
public NodeSubstitutePatternEditor createSubstitutePatternEditor() {
NodeSubstitutePatternEditor pattern = new NodeSubstitutePatternEditor();
pattern.setText(getText());
pattern.setCaretPosition(getCaretPosition());
return pattern;
}
public void selectWordOrAll() {
if (getTextLine().getStartTextSelectionPosition() != getTextLine().getEndTextSelectionPosition()) {
selectAll();
return;
}
int start = getPrevLocalHome(false);
int end = getNextLocalEnd(false);
if (start != end) {
select(start, end);
} else {
selectAll();
}
}
private void select(int start, int end) {
assert start <= end;
setSelectionStart(start);
setSelectionEnd(end);
}
private int getNextLocalEnd(boolean skipLeadingSpaces) {
int length = getText().length();
assert getCaretPosition() <= length;
for (int i = getCaretPosition(); i != length; ++i) {
if (Character.isWhitespace(getText().charAt(i))) {
if (skipLeadingSpaces) {
if (i == length - 1 || !Character.isWhitespace(getText().charAt(i + 1))) {
return i + 1;
}
} else {
return i;
}
}
}
return length;
}
private int getPrevLocalHome(boolean skipLeadingSpaces) {
assert getCaretPosition() >= 0;
for (int i = getCaretPosition(); i >= 1; --i) {
char c = getText().charAt(i - 1);
if (Character.isWhitespace(c) && !skipLeadingSpaces) {
return i;
}
if (!Character.isWhitespace(c)) {
skipLeadingSpaces = false;
}
}
return 0;
}
public void selectAll() {
getTextLine().selectAll();
}
@Override
public void deselectAll() {
getTextLine().deselectAll();
}
public boolean isEverythingSelected() {
return getTextLine().isEverythingSelected();
}
@Override
public SubstituteInfo getSubstituteInfo() {
SubstituteInfo substituteInfo = super.getSubstituteInfo();
if (substituteInfo != null) {
substituteInfo.setOriginalText(getText() == null || getText().equals("") ? getNullText() : getText());
}
return substituteInfo;
}
public String toString() {
return NameUtil.shortNameFromLongName(getClass().getName()) + "[text=" + getText() + "]";
}
@Override
public TextBuilder renderText() {
return new TextBuilderImpl(getRenderedText());
}
public int getCharWidth() {
return getRenderedTextLine().charWidth();
}
public String getTextBeforeCaret() {
return myTextLine.getTextBeforeCaret();
}
public String getTextAfterCaret() {
return myTextLine.getTextAfterCaret();
}
private void fireSelectionChanged() {
getEditorComponent().getSelectionManager().setSelection(getEditorComponent().getSelectionManager().getSelection());
}
private boolean isTheOnlyCompletelySelectedLabelInBigCell() {
jetbrains.mps.openapi.editor.cells.EditorCell containingBigCell = CellTraversalUtil.getContainingBigCell(this);
return CellTraversalUtil.getFirstLeaf(containingBigCell) == this && CellTraversalUtil.getLastLeaf(containingBigCell) == this &&
getText().equals(getSelectedText());
}
public String getCommandGroupId() {
return getCellId() + "_" + String.valueOf(getSNodeId());
}
private class MoveLeft extends AbstractCellAction {
private boolean myWithSelection;
private MoveLeft(boolean withSelection) {
myWithSelection = withSelection;
}
@Override
public boolean canExecute(EditorContext context) {
return isCaretPositionAllowed(getCaretPosition() - 1);
}
@Override
public void execute(EditorContext context) {
setCaretPosition(getCaretPosition() - 1, myWithSelection);
fireSelectionChanged();
ensureCaretVisible();
}
}
private class MoveRight extends AbstractCellAction {
private boolean myWithSelection;
private MoveRight(boolean withSelection) {
myWithSelection = withSelection;
}
@Override
public boolean canExecute(EditorContext context) {
return isCaretPositionAllowed(getCaretPosition() + 1);
}
@Override
public void execute(EditorContext context) {
setCaretPosition(getCaretPosition() + 1, myWithSelection);
fireSelectionChanged();
ensureCaretVisible();
}
}
private class SelectHome extends AbstractCellAction {
@Override
public boolean canExecute(EditorContext context) {
return isCaretPositionAllowed(0);
}
@Override
public void execute(EditorContext context) {
setCaretPosition(0, true);
fireSelectionChanged();
ensureCaretVisible();
}
}
private class SelectEnd extends AbstractCellAction {
@Override
public boolean canExecute(EditorContext context) {
return isCaretPositionAllowed(getText().length());
}
@Override
public void execute(EditorContext context) {
setCaretPosition(getText().length(), true);
fireSelectionChanged();
ensureCaretVisible();
}
}
private class CopyLabelText extends AbstractCellAction {
@Override
public boolean canExecute(EditorContext context) {
SelectionManager selectionManager = context.getEditorComponent().getSelectionManager();
if (selectionManager.getSelection() instanceof EditorCellLabelSelection) {
EditorCellLabelSelection labelSelection = (EditorCellLabelSelection) selectionManager.getSelection();
return labelSelection.getEditorCellLabel().getSelectedText().length() > 0;
}
return false;
}
@Override
public void execute(EditorContext context) {
// TODO: use EditorCell_Label.this. instead..
EditorCell_Label label = (EditorCell_Label) context.getSelectedCell();
if (label.isTheOnlyCompletelySelectedLabelInBigCell()) {
CopyPasteUtil.copyTextAndNodeToClipboard(label.getSelectedText(), getSNode());
} else {
CopyPasteUtil.copyTextToClipboard(label.getSelectedText());
}
}
}
private class LocalHome extends AbstractCellAction {
private boolean mySelect;
private LocalHome(boolean select) {
mySelect = select;
}
@Override
public boolean canExecute(EditorContext context) {
return !isFirstCaretPosition() && (StyleAttributesUtil.isFirstPositionAllowed(getStyle()) || getPrevLocalHome(true) != 0);
}
@Override
public void execute(EditorContext context) {
setCaretPosition(getPrevLocalHome(true), mySelect);
}
}
private class LocalEnd extends AbstractCellAction {
private boolean mySelect;
private LocalEnd(boolean select) {
mySelect = select;
}
@Override
public boolean canExecute(EditorContext context) {
return !isLastCaretPosition() && (StyleAttributesUtil.isLastPositionAllowed(getStyle()) || getNextLocalEnd(true) != getText().length());
}
@Override
public void execute(EditorContext context) {
setCaretPosition(getNextLocalEnd(true), mySelect);
}
}
private class ClearSelection extends AbstractCellAction {
@Override
public boolean canExecute(EditorContext context) {
return myTextLine.hasNonTrivialSelection();
}
@Override
public void execute(EditorContext context) {
myTextLine.resetSelection();
}
}
private class PasteIntoLabelText extends AbstractCellAction {
@Override
public boolean canExecute(EditorContext context) {
if (!(context.getSelectedCell() instanceof EditorCell_Label)) {
return false;
}
// TODO: use EditorCell_Label.this. instead..
EditorCell_Label label = (EditorCell_Label) context.getSelectedCell();
SNode node = label.getSNode();
// If selected cell is:
// - the only completely selected label in big cell
// - the cursor is in the beginning of this cell
// - the cursor is in the end of this cell
// then we paste text into the cell only if it is on top of clipboard (text was copied from another cell)
// otherwise in this case this action will not be applicable, so node paste action should perform actual pasting
//
// if non of above is true then just pasting text from the clipboard into this cell (e.g. you can copy 1 + 2 and
// paste it into the name label).
return node != null && label.canPasteText() && label.isEditable() &&
(label.isTheOnlyCompletelySelectedLabelInBigCell() || isFirstCaretPosition() && !getTextLine().hasNonTrivialSelection() ||
isLastCaretPosition() && !getTextLine().hasNonTrivialSelection() ? CopyPasteUtil.isStringOnTopOfClipboard() :
TextPasteUtil.hasStringInClipboard());
}
@Override
public void execute(EditorContext context) {
EditorCell_Label cell = (EditorCell_Label) context.getSelectedCell();
final String s = TextPasteUtil.getStringFromClipboard();
cell.insertText(NameUtil.escapeInvisibleCharacters(s));
fireSelectionChanged();
cell.ensureCaretVisible();
}
}
private class CutLabelText extends AbstractCellAction {
@Override
public boolean canExecute(EditorContext context) {
SelectionManager selectionManager = context.getEditorComponent().getSelectionManager();
if (selectionManager.getSelection() instanceof EditorCellLabelSelection) {
EditorCellLabelSelection labelSelection = (EditorCellLabelSelection) selectionManager.getSelection();
return labelSelection.getEditorCellLabel().getSelectedText().length() > 0;
}
return false;
}
@Override
public void execute(EditorContext context) {
// TODO: use EditorCell_Label.this. instead..
EditorCell_Label label = (EditorCell_Label) context.getSelectedCell();
if (label.isTheOnlyCompletelySelectedLabelInBigCell()) {
CopyPasteUtil.copyTextAndNodeToClipboard(label.getSelectedText(), getSNode());
} else {
CopyPasteUtil.copyTextToClipboard(label.getSelectedText());
}
if (label.canPasteText()) {
label.deleteSelection();
}
}
}
private class ProcessTextActionCommand extends EditorComputable<Boolean> implements UndoRunnable {
private CellActionType myActionType;
private boolean myAllowErrors;
ProcessTextActionCommand(EditorContext context, CellActionType type, boolean allowErrors) {
super(context);
myActionType = type;
myAllowErrors = allowErrors;
}
@Override
public Boolean doCompute() {
String oldText = myTextLine.getText();
int caretPosition = myTextLine.getCaretPosition();
if (myActionType == CellActionType.BACKSPACE) {
if (myTextLine.hasNonTrivialSelection()) {
deleteSelection();
deleteIfPossible(myActionType);
return true;
}
if (caretPosition > 0) {
String newText = oldText.substring(0, caretPosition - 1) + oldText.substring(caretPosition);
if (!myAllowErrors && !isValidText(newText)) {
return false;
}
changeText(newText);
addChangeTextUndoableAction();
if (!isCaretPositionAllowed(caretPosition - 1)) {
return false;
}
setCaretPosition(caretPosition - 1);
ensureCaretVisible();
deleteIfPossible(myActionType);
return true;
} else {
jetbrains.mps.openapi.editor.cells.EditorCell prevLeaf = CellTraversalUtil.getPrevLeaf(EditorCell_Label.this);
if (myAllowErrors && canDeleteFrom(prevLeaf)) {
EditorCell_Label label = (EditorCell_Label) prevLeaf;
getEditor().changeSelection(label);
label.end();
label.executeTextAction(myActionType, true);
return true;
}
return false;
}
} else if (myActionType == CellActionType.DELETE) {
if (myTextLine.hasNonTrivialSelection()) {
deleteSelection();
deleteIfPossible(myActionType);
return true;
} else if (caretPosition < oldText.length()) {
String newText = oldText.substring(0, caretPosition) + oldText.substring(caretPosition + 1);
if (!myAllowErrors && !isValidText(newText)) {
return false;
}
changeText(newText);
addChangeTextUndoableAction();
ensureCaretVisible();
deleteIfPossible(myActionType);
return true;
} else {
jetbrains.mps.openapi.editor.cells.EditorCell nextLeaf = CellTraversalUtil.getNextLeaf(EditorCell_Label.this);
if (myAllowErrors && canDeleteFrom(nextLeaf)) {
EditorCell_Label label = (EditorCell_Label) nextLeaf;
getEditor().changeSelection(label);
label.home();
label.executeTextAction(myActionType, true);
return true;
}
return false;
}
}
return false;
}
@Nullable
@Override
public String getName() {
return null;
}
@Nullable
@Override
public String getGroupId() {
return getCommandGroupId();
}
@Override
public boolean shallConfirmUndo() {
return false;
}
}
public class ModifyTextCommand extends EditorComputable<Boolean> implements UndoRunnable {
private final String myReplacingText;
private final boolean myAllowErrors;
private final CellSide mySide;
public ModifyTextCommand(String replacingText, boolean allowErrors, CellSide side, EditorContext context) {
super(context);
myReplacingText = replacingText;
myAllowErrors = allowErrors;
mySide = side;
}
@Override
protected Boolean doCompute() {
if (processMutableKeyTyped(myReplacingText, myAllowErrors)) {
getContext().flushEvents();
addChangeTextUndoableAction();
if (isErrorState() && mySide != null && IntelligentInputUtil.processCell(EditorCell_Label.this, getContext(), getRenderedText(), mySide)) {
/**
* Resetting current command group ID if cell was side-transformed. In such situations
* side-transforming command as well as char typing command should be separate part of
* undo-redo process, not connected with eytyping events which are grouped together.
*/
CommandProcessor.getInstance().setCurrentCommandGroupId(null);
}
return true;
}
return isErrorState() && mySide == CellSide.LEFT && " ".equals(myReplacingText);
}
private boolean processMutableKeyTyped(String replacement, final boolean allowErrors) {
String newText = getUpdatedText(replacement);
if (!allowErrors && !isValidText(newText)) {
return false;
}
int startSelection = myTextLine.getStartTextSelectionPosition();
changeText(newText);
setCaretPositionIfPossible(startSelection + myReplacingText.length());
myTextLine.resetSelection();
fireSelectionChanged();
ensureCaretVisible();
return true;
}
@Nullable
@Override
public String getName() {
return null;
}
@Nullable
@Override
public String getGroupId() {
return getCommandGroupId();
}
@Override
public boolean shallConfirmUndo() {
return false;
}
}
/**
* This action can be used to introduce empty action into the stack of actions within UndoHelper
* forcing it to add undoable command into IDEA undo stack: see {@link jetbrains.mps.ide.undo.WorkbenchUndoHandler}
* flushCommand() method implementation. This method will not add {@link jetbrains.mps.ide.undo.SNodeIdeaUndoableAction}
* action into IDEA undo stack if it has no own undoable actions.
* <p/>
* This is helpful in case of UI-only modifications performed upon the cells. For example, if the textual cell is modified
* so it represents invalid value. In this case this value cannot be committed into the model and will stay in the editor
* cell/memento objects only. Empty command in this case will add a "mark" in IDEA undo stack, so corresponding editor
* memento will be restored on udo/redo of this empty command.
*/
protected static class DummyUndoableAction extends SNodeUndoableAction {
protected DummyUndoableAction(SNode node) {
super(node);
}
@Override
protected void doUndo() {
}
@Override
protected void doRedo() {
}
}
}