/*
* 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;
import jetbrains.mps.nodeEditor.cells.DefaultCellInfo;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.nodeEditor.cells.EditorCell_Property;
import jetbrains.mps.nodeEditor.cells.TransactionalPropertyAccessor;
import jetbrains.mps.nodeEditor.selection.SelectionInfoImpl;
import jetbrains.mps.openapi.editor.EditorComponentState;
import jetbrains.mps.openapi.editor.EditorContext;
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.cells.optional.WithCaret;
import jetbrains.mps.openapi.editor.selection.Selection;
import jetbrains.mps.openapi.editor.selection.SelectionInfo;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.util.EqualUtil;
import jetbrains.mps.util.Pair;
import org.jdom.Attribute;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
class Memento implements EditorComponentState {
private static final Comparator<Pair<EditorCell_Collection, Boolean>> COLLAPSED_STATES_COMPARATOR = new Comparator<Pair<EditorCell_Collection, Boolean>>() {
@Override
public int compare(Pair<EditorCell_Collection, Boolean> p1,
Pair<EditorCell_Collection, Boolean> p2) {
int depthDelta = getDepth(p2.o1) - getDepth(p1.o1);
return depthDelta != 0 ? depthDelta : CELL_COMPARATOR.compare(p2.o1, p1.o1);
}
private int getDepth(EditorCell cell) {
int depth = 0;
while (cell.getParent() != null) {
cell = cell.getParent();
depth++;
}
return depth;
}
};
private static final Comparator<EditorCell> CELL_COMPARATOR =
(cell1, cell2) -> CellTraversalUtil.getCommonParent(cell1, cell2) == null ? 0 : CellTraversalUtil.compare(cell1, cell2);
private List<SelectionInfo> mySelectionStack = new ArrayList<>();
private List<CellInfo> myCollectionsWithEnabledBraces = new ArrayList<>();
private List<Pair<CellInfo, Boolean>> myFoldableStates = new ArrayList<>();
private List<Pair<CellInfo, Boolean>> myInitiallyCollapsedStates = new ArrayList<>();
private List<Pair<CellInfo, Boolean>> myRestoreAlwaysStates = new ArrayList<>();
private List<ErrorMarker> myErrors = new ArrayList<>();
private List<TransactionalPropertyState> myTransactionalProperties = new ArrayList<>();
private Point myViewPosition;
private String[] myEnabledHints = null;
private SNodeReference myEditedNodeReference;
private boolean mySaveSessionState = true;
private Memento() {
}
@Override
public void clearSessionState() {
mySaveSessionState = false;
}
Memento(EditorContext context, boolean saveEditedNode) {
EditorComponent nodeEditor = (EditorComponent) context.getEditorComponent();
SNode editedNode = nodeEditor.getEditedNode();
if (editedNode == null || SNodeUtil.isAccessible(editedNode, context.getRepository())) {
if (saveEditedNode && editedNode != null) {
myEditedNodeReference = editedNode.getReference();
}
mySelectionStack = nodeEditor.getSelectionManager().getSelectionInfoStack();
getFoldableStates(collectRestoreAlways(nodeEditor, mySelectionStack)).forEach(myRestoreAlwaysStates::add);
getFoldableStates(collectFoldable(nodeEditor, false)).forEach(myFoldableStates::add);
getFoldableStates(collectFoldable(nodeEditor, true)).forEach(myInitiallyCollapsedStates::add);
nodeEditor.getBracesEnabledCells().stream().sorted(CELL_COMPARATOR).map(EditorCell::getCellInfo).forEach(myCollectionsWithEnabledBraces::add);
// collect errors
nodeEditor.getCellTracker().getErrorCells().stream().sorted(CELL_COMPARATOR).filter(
cell -> cell instanceof EditorCell_Label && ((EditorCell_Label) cell).isEditable()).map(cell -> new ErrorMarker((EditorCell_Label) cell)).forEach(
myErrors::add);
// collect transactionals
nodeEditor.getCellTracker().getTransactionalCells().stream().sorted(CELL_COMPARATOR).filter(
cell -> ((TransactionalPropertyAccessor) cell.getModelAccessor()).hasValueToCommit()).map(TransactionalPropertyState::new).forEach(
myTransactionalProperties::add);
}
myViewPosition = nodeEditor.getViewPosition();
myEnabledHints = nodeEditor.getUpdater().getInitialEditorHints();
}
@NotNull
private Stream<EditorCell_Collection> collectRestoreAlways(EditorComponent nodeEditor, List<SelectionInfo> selectionInfoStack) {
Set<EditorCell_Collection> visitedCollections = new HashSet<>();
List<EditorCell_Collection> result = new ArrayList<>();
for (SelectionInfo selectionInfo : selectionInfoStack) {
Selection selection = selectionInfo.createSelection(nodeEditor);
if (selection == null) {
continue;
}
for (EditorCell nextCell : selection.getSelectedCells()) {
for (EditorCell_Collection parent = nextCell.getParent(); parent != null; parent = parent.getParent()) {
if (visitedCollections.add(parent) && parent.isFoldable()) {
result.add(parent);
}
}
}
}
return result.stream();
}
private Stream<EditorCell_Collection> collectFoldable(EditorComponent nodeEditor, boolean initiallyCollapsed) {
return nodeEditor.getCellTracker().getFoldableCells().stream().map(EditorCell_Collection.class::cast).filter(
cell -> initiallyCollapsed == cell.isInitiallyCollapsed());
}
private Stream<Pair<CellInfo, Boolean>> getFoldableStates(Stream<EditorCell_Collection> cells) {
return cells.map(cell -> new Pair<>(cell, cell.isCollapsed())).sorted(COLLAPSED_STATES_COMPARATOR).map(
collapsedState -> new Pair<>(collapsedState.o1.getCellInfo(), collapsedState.o2));
}
void restore(EditorComponent editor) {
boolean editorRebuildRequired = editor.getUpdater().setInitialEditorHints(myEnabledHints);
if (myEditedNodeReference != null) {
SNode newEditedNode = myEditedNodeReference.resolve(editor.getEditorContext().getRepository());
if (newEditedNode != null && editor.getEditedNode() != newEditedNode) {
editor.editNode(newEditedNode);
editor.getUpdater().flushModelEvents();
editorRebuildRequired = false;
}
}
if (editorRebuildRequired) {
editor.rebuildEditorContent();
editor.getUpdater().flushModelEvents();
}
editor.clearBracesEnabledCells();
editor.getUpdater().flushModelEvents();
// TODO: remove this variable and simply mark editor as "needsRelayout" from the top editor cell + relayout it on .. next paint?
boolean needsRelayout = restoreErrors(editor) | restoreTransactionals(editor);
// Restore collapse states before restoring selection, otherwise selection inside initially collapsed cells disappears
needsRelayout = restoreFoldingStates(myFoldableStates, editor) | needsRelayout;
needsRelayout = restoreFoldingStates(mySaveSessionState ? myInitiallyCollapsedStates : myRestoreAlwaysStates, editor) | needsRelayout;
editor.getSelectionManager().setSelectionInfoStack(mySelectionStack);
EditorCell selectedCell = editor.getDeepestSelectedCell();
if (selectedCell instanceof WithCaret) {
((WithCaret) selectedCell).setCaretVisible(false);
}
for (CellInfo collectionInfo : myCollectionsWithEnabledBraces) {
EditorCell collection = collectionInfo.findCell(editor);
if (!(collection instanceof jetbrains.mps.nodeEditor.cells.EditorCell_Collection)) {
continue;
}
if (((jetbrains.mps.nodeEditor.cells.EditorCell_Collection) collection).usesBraces()) {
((jetbrains.mps.nodeEditor.cells.EditorCell_Collection) collection).enableBraces();
}
}
if (needsRelayout) {
editor.relayout();
}
if (myViewPosition != null) {
editor.setViewPosition(myViewPosition);
}
}
private boolean restoreFoldingStates(Iterable<Pair<CellInfo, Boolean>> foldingStates, EditorComponent editor) {
boolean needsRelayout = false;
for (Pair<CellInfo, Boolean> collapseState : foldingStates) {
EditorCell cell = collapseState.o1.findCell(editor);
if (!(cell instanceof EditorCell_Collection)) {
continue;
}
EditorCell_Collection collection = (EditorCell_Collection) cell;
needsRelayout = true;
if (collapseState.o2) {
collection.fold();
} else {
collection.unfold();
}
}
return needsRelayout;
}
private boolean restoreErrors(EditorComponent editor) {
boolean needsRelayout = false;
for (EditorCell cell : new ArrayList<EditorCell>(editor.getCellTracker().getErrorCells())) {
if (cell instanceof EditorCell_Label && ((EditorCell_Label) cell).isEditable()) {
EditorCell_Label label = (EditorCell_Label) cell;
label.synchronizeViewWithModel();
needsRelayout = true;
}
}
for (ErrorMarker error : myErrors) {
needsRelayout = error.restore(editor) || needsRelayout;
}
return needsRelayout;
}
private boolean restoreTransactionals(EditorComponent editor) {
boolean needsRelayout = false;
for (EditorCell_Property transactionalProperty : editor.getCellTracker().getTransactionalCells()) {
if (transactionalProperty.getModelAccessor() instanceof TransactionalPropertyAccessor) {
TransactionalPropertyAccessor accessor = (TransactionalPropertyAccessor) transactionalProperty.getModelAccessor();
if (accessor.hasValueToCommit()) {
accessor.resetUncommittedValue();
transactionalProperty.synchronize();
}
}
}
for (TransactionalPropertyState transactionalProperty : myTransactionalProperties) {
needsRelayout = transactionalProperty.restore(editor) || needsRelayout;
}
return needsRelayout;
}
public boolean equals(Object object) {
if (object == this) {
return true;
}
if (object instanceof Memento) {
Memento m = (Memento) object;
if (EqualUtil.equals(mySelectionStack, m.mySelectionStack) && EqualUtil.equals(myCollectionsWithEnabledBraces, m.myCollectionsWithEnabledBraces) &&
EqualUtil.equals(myFoldableStates, m.myFoldableStates) && EqualUtil.equals(myInitiallyCollapsedStates, m.myInitiallyCollapsedStates) &&
EqualUtil.equals(myRestoreAlwaysStates, m.myRestoreAlwaysStates) && EqualUtil.equals(myErrors, m.myErrors) &&
EqualUtil.equals(myTransactionalProperties, m.myTransactionalProperties) && EqualUtil.equals(myViewPosition, m.myViewPosition) &&
Arrays.equals(myEnabledHints, m.myEnabledHints) && EqualUtil.equals(myEditedNodeReference, m.myEditedNodeReference)) {
return true;
}
}
return false;
}
public int hashCode() {
return (mySelectionStack != null ? mySelectionStack.hashCode() : 0);
}
public String toString() {
return "Editor Memento[\n" +
" selectedStack = " + mySelectionStack + "\n" +
" collectionsWithBraces = " + myCollectionsWithEnabledBraces + "\n" +
" foldableCells = " + myFoldableStates + "\n" +
" collapsedCells = " + myInitiallyCollapsedStates + "\n" +
" expandAlwaysCells = " + myRestoreAlwaysStates + "\n" +
" enabledHints = " + Arrays.toString(myEnabledHints) + "\n" +
" editedNodeReference = " + myEditedNodeReference + "\n" +
"]\n";
}
private static final String SELECTION_STACK = "selectionStack";
private static final String STACK_ELEMENT = "stackElement";
private static final String FOLDABLE = "foldable";
private static final String INITIALLY_COLLAPSED = "initiallyCollapsed";
private static final String RESTORE_ALWAYS = "restoreAlways";
private static final String COLLAPSED_ELEMENT = "collapsedElement";
private static final String CELL_ID_ELEMENT = "cellIdElement";
private static final String COLLAPSED_VALUE = "isCollapsed";
private static final String VIEW_POSITION_X = "viewPositionX";
private static final String VIEW_POSITION_Y = "viewPositionY";
private static final String ENABLED_HINTS = "enabledHints";
private static final String ENABLED_HINTS_ELEMENT = "enabledHintsElement";
private static final String ENABLED_HINTS_ATTRIBUTE = "enabledHintsAttribute";
private static final String ERROR_MARKERS = "errorMarkers";
private static final String TRANSACTIONAL_PROPERTIES = "transactionalProperties";
private static final String EDITED_NODE = "currentlyEditedNode";
private static final String SAVE_SESSION_STATE = "saveSessionState";
public void save(Element e) {
if (myEditedNodeReference != null) {
e.setAttribute(EDITED_NODE, SNodePointer.serialize(myEditedNodeReference));
}
e.setAttribute(SAVE_SESSION_STATE, Boolean.toString(mySaveSessionState));
Element selectionStack = new Element(SELECTION_STACK);
e.addContent(selectionStack);
for (SelectionInfo selectionInfo : mySelectionStack) {
Element stackElement = new Element(STACK_ELEMENT);
((SelectionInfoImpl) selectionInfo).persistToXML(stackElement);
selectionStack.addContent(stackElement);
}
Element errorMarkers = new Element(ERROR_MARKERS);
e.addContent(errorMarkers);
for (ErrorMarker error : myErrors) {
error.save(errorMarkers);
}
Element transactionalProperties = new Element(TRANSACTIONAL_PROPERTIES);
e.addContent(transactionalProperties);
for (TransactionalPropertyState transactionalProperty : myTransactionalProperties) {
transactionalProperty.save(transactionalProperties);
}
saveFoldingStates(new Element(FOLDABLE), e, myFoldableStates);
if (mySaveSessionState) {
saveFoldingStates(new Element(INITIALLY_COLLAPSED), e, myInitiallyCollapsedStates);
}
saveFoldingStates(new Element(RESTORE_ALWAYS), e, myRestoreAlwaysStates);
e.setAttribute(VIEW_POSITION_X, String.valueOf(myViewPosition.x));
e.setAttribute(VIEW_POSITION_Y, String.valueOf(myViewPosition.y));
if (myEnabledHints != null) {
Element hintsElement = new Element(ENABLED_HINTS);
for (String hint : myEnabledHints) {
Element hintElement = new Element(ENABLED_HINTS_ELEMENT);
hintElement.setAttribute(ENABLED_HINTS_ATTRIBUTE, hint);
hintsElement.addContent(hintElement);
}
e.addContent(hintsElement);
}
}
private static void saveFoldingStates(Element element, Element parentElement, Iterable<Pair<CellInfo, Boolean>> foldingStates) {
for (Pair<CellInfo, Boolean> collapsedState : foldingStates) {
if (collapsedState.o1 instanceof DefaultCellInfo) {
Element collapsedElement = new Element(COLLAPSED_ELEMENT);
collapsedElement.setAttribute(COLLAPSED_VALUE, collapsedState.o2.toString());
Element cellId = new Element(CELL_ID_ELEMENT);
((DefaultCellInfo) collapsedState.o1).saveTo(cellId);
collapsedElement.addContent(cellId);
element.addContent(collapsedElement);
} else {
return;
}
}
parentElement.addContent(element);
}
public static Memento load(Element e) {
Memento memento = new Memento();
Attribute editedNodeAttribute = e.getAttribute(EDITED_NODE);
if (editedNodeAttribute != null) {
memento.myEditedNodeReference = SNodePointer.deserialize(editedNodeAttribute.getValue());
}
memento.mySaveSessionState = Boolean.parseBoolean(e.getAttributeValue(SAVE_SESSION_STATE));
Element selectionStack = e.getChild(SELECTION_STACK);
if (selectionStack != null) {
selectionStack.getChildren(STACK_ELEMENT).stream().map(SelectionInfoImpl::new).forEach(memento.mySelectionStack::add);
}
Element errorMarkers = e.getChild(ERROR_MARKERS);
if (errorMarkers != null) {
ErrorMarker.loadMarkers(errorMarkers).forEach(memento.myErrors::add);
}
Element transactionalProperties = e.getChild(TRANSACTIONAL_PROPERTIES);
if (transactionalProperties != null) {
TransactionalPropertyState.load(transactionalProperties).forEach(memento.myTransactionalProperties::add);
}
loadFoldingStates(e.getChild(FOLDABLE), memento.myFoldableStates);
loadFoldingStates(e.getChild(INITIALLY_COLLAPSED), memento.myInitiallyCollapsedStates);
loadFoldingStates(e.getChild(RESTORE_ALWAYS), memento.myRestoreAlwaysStates);
try {
int viewPositionX = Integer.valueOf(e.getAttributeValue(VIEW_POSITION_X));
int viewPositionY = Integer.valueOf(e.getAttributeValue(VIEW_POSITION_Y));
memento.myViewPosition = new Point(viewPositionX, viewPositionY);
} catch (NumberFormatException nfe) {
}
Element hintsElement = e.getChild(ENABLED_HINTS);
if (hintsElement != null) {
List<String> enabledHints = new ArrayList<>();
List children = hintsElement.getChildren(ENABLED_HINTS_ELEMENT);
for (Object o : children) {
enabledHints.add(((Element) o).getAttributeValue(ENABLED_HINTS_ATTRIBUTE));
}
memento.myEnabledHints = enabledHints.toArray(new String[enabledHints.size()]);
}
return memento;
}
private static void loadFoldingStates(Element element, List<Pair<CellInfo, Boolean>> result) {
if (element == null) {
return;
}
element.getChildren(COLLAPSED_ELEMENT).stream().map(el -> new Pair<CellInfo, Boolean>(DefaultCellInfo.loadFrom(el.getChild(CELL_ID_ELEMENT)),
Boolean.valueOf(el.getAttributeValue(COLLAPSED_VALUE)))).forEach(result::add);
}
private static class ErrorMarker {
private static final String ERROR_MARKER = "errorMarker";
private static final String TEXT = "text";
private static final String MODEL_TEXT = "modelText";
private static final String PROPERTY_CELL = "propertyCell";
private CellInfo myCellInfo;
private String myText;
private String myModelText = null;
private boolean myPropertyCell = false;
ErrorMarker(EditorCell_Label label) {
myText = label.getText();
myCellInfo = label.getCellInfo();
if (label instanceof EditorCell_Property) {
myModelText = ((EditorCell_Property) label).getLastModelText();
myPropertyCell = true;
}
}
private ErrorMarker(Element errorElement) {
myText = errorElement.getAttributeValue(TEXT);
myModelText = errorElement.getAttributeValue(MODEL_TEXT);
myPropertyCell = Boolean.parseBoolean(errorElement.getAttributeValue(PROPERTY_CELL));
myCellInfo = DefaultCellInfo.loadFrom(errorElement);
}
private boolean isPropertyCell() {
return myPropertyCell;
}
public boolean restore(EditorComponent editor) {
EditorCell cell = myCellInfo.findCell(editor);
if (!(cell instanceof EditorCell_Label)) {
return false;
}
EditorCell_Label cellLabel = (EditorCell_Label) cell;
if (!cellLabel.isEditable()) {
return false;
}
return isPropertyCell() ? restorePropertyCell(cellLabel) : restoreLabelCell(cellLabel);
}
private boolean restoreLabelCell(EditorCell_Label cellLabel) {
if (canRestoreText(cellLabel)) {
cellLabel.changeText(myText);
return true;
}
return false;
}
private boolean restorePropertyCell(EditorCell_Label cell) {
if (!(cell instanceof EditorCell_Property)) {
return false;
}
EditorCell_Property cellProperty = (EditorCell_Property) cell;
if (EqualUtil.equals(myModelText, cellProperty.getLastModelText()) && canRestoreText(cellProperty)) {
cellProperty.changeText(myText);
return true;
}
return false;
}
private boolean canRestoreText(EditorCell_Label cellLabel) {
return !EqualUtil.equals(cellLabel.getText(), myText) && !cellLabel.isValidText(myText);
}
public void save(Element errorMarkers) {
Element errorElement = new Element(ERROR_MARKER);
errorElement.setAttribute(TEXT, myText);
if (myModelText != null) {
errorElement.setAttribute(MODEL_TEXT, myModelText);
}
errorElement.setAttribute(PROPERTY_CELL, Boolean.toString(myPropertyCell));
((DefaultCellInfo) myCellInfo).saveTo(errorElement);
errorMarkers.addContent(errorElement);
}
static Stream<ErrorMarker> loadMarkers(Element errorMarkers) {
return errorMarkers.getChildren(ERROR_MARKER).stream().map(ErrorMarker::new);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ErrorMarker that = (ErrorMarker) o;
if (myPropertyCell != that.myPropertyCell) {
return false;
}
if (!myCellInfo.equals(that.myCellInfo)) {
return false;
}
if (!myText.equals(that.myText)) {
return false;
}
return !(myModelText != null ? !myModelText.equals(that.myModelText) : that.myModelText != null);
}
@Override
public int hashCode() {
return myCellInfo.hashCode();
}
}
private static class TransactionalPropertyState {
private static final String UNCOMMITTED_VALUE = "uncommittedValue";
private static final String TRANSACTIONAL_PROPERTY = "transactionalProperty";
private final String myUncommittedValue;
private final CellInfo myCellInfo;
TransactionalPropertyState(EditorCell_Property propertyCell) {
TransactionalPropertyAccessor accessor = (TransactionalPropertyAccessor) propertyCell.getModelAccessor();
assert accessor.hasValueToCommit();
myUncommittedValue = accessor.doGetValue();
myCellInfo = propertyCell.getCellInfo();
}
private TransactionalPropertyState(Element transactionalElement) {
myUncommittedValue = transactionalElement.getAttributeValue(UNCOMMITTED_VALUE);
myCellInfo = DefaultCellInfo.loadFrom(transactionalElement);
}
public boolean restore(EditorComponent editor) {
EditorCell cell = myCellInfo.findCell(editor);
if (!(cell instanceof EditorCell_Property)) {
return false;
}
EditorCell_Property propertyCell = (EditorCell_Property) cell;
if (propertyCell.getModelAccessor() instanceof TransactionalPropertyAccessor) {
TransactionalPropertyAccessor modelAccessor = (TransactionalPropertyAccessor) propertyCell.getModelAccessor();
modelAccessor.doSetValue(myUncommittedValue);
propertyCell.synchronize();
return true;
}
return false;
}
public void save(Element transactionalProperties) {
Element transactionalElement = new Element(TRANSACTIONAL_PROPERTY);
transactionalElement.setAttribute(UNCOMMITTED_VALUE, myUncommittedValue);
((DefaultCellInfo) myCellInfo).saveTo(transactionalElement);
transactionalProperties.addContent(transactionalElement);
}
public static Stream<TransactionalPropertyState> load(Element transactionalProperties) {
return transactionalProperties.getChildren(TRANSACTIONAL_PROPERTY).stream().map(TransactionalPropertyState::new);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TransactionalPropertyState that = (TransactionalPropertyState) o;
return myCellInfo.equals(that.myCellInfo) &&
(myUncommittedValue == null ? that.myUncommittedValue == null : myUncommittedValue.equals(that.myUncommittedValue));
}
@Override
public int hashCode() {
return myCellInfo.hashCode();
}
}
}