/*
* 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.editor.runtime.SideTransformInfoUtil;
import jetbrains.mps.editor.runtime.style.StyleAttributes;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.AttributeOperations;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.nodeEditor.cellActions.SideTransformSubstituteInfo.Side;
import jetbrains.mps.nodeEditor.cells.CellFinderUtil;
import jetbrains.mps.nodeEditor.cells.EditorCell_Collection;
import jetbrains.mps.nodeEditor.cells.EditorCell_Error;
import jetbrains.mps.nodeEditor.cells.SynchronizeableEditorCell;
import jetbrains.mps.nodeEditor.sidetransform.EditorCell_STHint;
import jetbrains.mps.nodeEditor.updater.UpdaterImpl;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.CellInfo;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCellContext;
import jetbrains.mps.openapi.editor.cells.EditorCellFactory;
import jetbrains.mps.openapi.editor.update.AttributeKind;
import jetbrains.mps.openapi.editor.update.UpdateSession;
import jetbrains.mps.openapi.editor.update.Updater;
import jetbrains.mps.smodel.NodeReadAccessCasterInEditor;
import jetbrains.mps.smodel.NodeReadAccessInEditorListener;
import jetbrains.mps.smodel.SNodeUtil;
import jetbrains.mps.util.Pair;
import jetbrains.mps.util.SNodeOperations;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;
public class EditorManager {
private static final Logger LOG = Logger.wrap(LogManager.getLogger(EditorManager.class));
private static final String BIG_CELL_CONTEXT = "big-cell-context";
public static final String OLD_NODE_FOR_SUBSTITUTION = "oldNode";
private final EditorContext myEditorContext;
private Deque<Map<ReferencedNodeContext, EditorCell>> myContextToOldCellMap = new LinkedList<Map<ReferencedNodeContext, EditorCell>>();
private boolean myCreatingInspectedCell = false;
private Stack<SNode> myAttributesStack = new Stack<SNode>();
@Nullable
public static EditorManager getInstanceFromContext(EditorContext editorContext) {
// TODO: Create API interface for EditorManager & use this method always here
return ((jetbrains.mps.nodeEditor.EditorContext) editorContext).getEditorManager();
}
public EditorManager(EditorContext editorContext) {
myEditorContext = editorContext;
}
private EditorContext getEditorContext() {
return myEditorContext;
}
private Updater getUpdater() {
return getEditorContext().getEditorComponent().getUpdater();
}
// TODO: remove this method, use getUpdater()
private UpdaterImpl getUpdaterImpl() {
return (UpdaterImpl) getEditorContext().getEditorComponent().getUpdater();
}
private UpdateSession getUpdateSession() {
return getUpdater().getCurrentUpdateSession();
}
private EditorCellFactory getCellFactory() {
return getUpdateSession().getCellFactory();
}
// TODO: make package-local, move to jetbrains.mps.nodeEditor.updater package ?
public EditorCell createRootCell(SNode node, List<Pair<SNode, SNodeReference>> modifications, ReferencedNodeContext refContext, boolean isInspectorCell) {
try {
pushTask("Creating " + (isInspectorCell ? "inspector" : "root") + " cell");
EditorCell rootCell = getEditorContext().getEditorComponent().getRootCell();
assert myContextToOldCellMap.isEmpty();
myContextToOldCellMap.push(new HashMap<ReferencedNodeContext, EditorCell>());
if (rootCell != null && modifications != null) {
fillContextToCellMap(rootCell, myContextToOldCellMap.peek());
}
myCreatingInspectedCell = isInspectorCell;
return createEditorCell(modifications, refContext);
} finally {
myContextToOldCellMap.pop();
popTask();
}
}
private static void fillContextToCellMap(EditorCell cell, Map<ReferencedNodeContext, EditorCell> map) {
Object bigCellContext = cell.getUserObject(BIG_CELL_CONTEXT);
if (bigCellContext instanceof ReferencedNodeContext) {
ReferencedNodeContext refContext = (ReferencedNodeContext) bigCellContext;
if (!map.containsKey(refContext)) {
map.put(refContext, cell);
}
// Don't go deeper if this cell represents normal node.
//
// Go deeper if this cell represents attribute node
// since we need to load mappings for all child attributes
// till the "main" node's cell and main node's cell itself.
if (!refContext.isNodeAttribute()) {
return;
}
}
fillContextToCellMapForChildren(cell, map);
}
private static void fillContextToCellMapForChildren(EditorCell cell, Map<ReferencedNodeContext, EditorCell> map) {
if (cell instanceof EditorCell_Collection) {
for (jetbrains.mps.openapi.editor.cells.EditorCell childCell : ((EditorCell_Collection) cell)) {
fillContextToCellMap(childCell, map);
}
}
}
public EditorCell createNodeRoleAttributeCell(SNode roleAttribute, AttributeKind attributeKind, EditorCell cellWithRole) {
// TODO: Make processing of style attributes more generic.
EditorCell attributeCell = getUpdateSession().updateAttributeCell(attributeKind, cellWithRole, roleAttribute);
// see a comment for isAttributedCell() method
if (attributeCell == cellWithRole) {
return cellWithRole;
}
if (cellWithRole.getStyle().get(StyleAttributes.INDENT_LAYOUT_NEW_LINE)) {
attributeCell.getStyle().set(StyleAttributes.INDENT_LAYOUT_NEW_LINE, true);
}
UpdaterImpl updater = getUpdaterImpl();
Set<SNode> newAttributeCell_DependOn = new HashSet<SNode>();
Set<SNode> attributeCell_DependOn = updater.getRelatedNodes(attributeCell);
if (attributeCell_DependOn != null) {
newAttributeCell_DependOn.addAll(attributeCell_DependOn);
}
Set<SNodeReference> newAttributeCell_RefTargetsDependsOn = new HashSet<SNodeReference>();
Set<SNodeReference> attributeCell_RefTargetsDependsOn = updater.getRelatedRefTargets(attributeCell);
if (attributeCell_RefTargetsDependsOn != null) {
newAttributeCell_RefTargetsDependsOn.addAll(attributeCell_RefTargetsDependsOn);
}
Set<SNode> cellWithRole_DependOn = updater.getRelatedNodes(cellWithRole);
if (cellWithRole_DependOn != null) {
newAttributeCell_DependOn.addAll(cellWithRole_DependOn);
}
Set<SNodeReference> cellWithRole_RefTargetsDependsOn = updater.getRelatedRefTargets(cellWithRole);
if (cellWithRole_RefTargetsDependsOn != null) {
newAttributeCell_RefTargetsDependsOn.addAll(cellWithRole_RefTargetsDependsOn);
}
if (attributeKind != AttributeKind.NODE) {
NodeReadAccessInEditorListener readAccessListener = NodeReadAccessCasterInEditor.getReadAccessListener();
if (readAccessListener != null) {
newAttributeCell_DependOn.addAll(readAccessListener.getNodesToDependOn());
newAttributeCell_RefTargetsDependsOn.addAll(readAccessListener.getRefTargetsToDependOn());
}
}
getUpdateSession().registerDependencies(attributeCell, newAttributeCell_DependOn, newAttributeCell_RefTargetsDependsOn);
return attributeCell;
}
protected boolean areAttributesShown() {
return !myCreatingInspectedCell;
}
// TODO: make package-local, move to jetbrains.mps.nodeEditor.updater package ?
public EditorCell createEditorCell(List<Pair<SNode, SNodeReference>> modifications, ReferencedNodeContext refContext) {
pushTask(getMessage(refContext, "?"));
try {
SNode node = refContext.getNode();
if (areAttributesShown()) {
for (SNode attribute : AttributeOperations.getNodeAttributes(node)) {
assert attribute != null;
// processing each attribute of current node just one time
// (creating cell tree for attributes & node recursively)
if (!myAttributesStack.contains(attribute)) {
myAttributesStack.push(attribute);
EditorCell nodeCell = createEditorCell(modifications, refContext);
SNode poppedAttribute = myAttributesStack.pop();
LOG.assertLog(poppedAttribute == attribute, "Assertion failed.");
return createNodeRoleAttributeCell(attribute, AttributeKind.NODE, nodeCell);
}
}
}
UpdaterImpl updater = getUpdaterImpl();
Map<ReferencedNodeContext, EditorCell> childContextToCellMap = null;
EditorCell oldCell = null;
if (modifications != null) {
oldCell = myContextToOldCellMap.peek().remove(refContext);
boolean nodeChanged = isNodeChanged(modifications, updater, oldCell, getCellFactory().getCellContext());
if (!nodeChanged) {
if (oldCell != null) {
final Set<SNode> nodesOldCellDependsOn = updater.getRelatedNodes(oldCell);
final Set<SNodeReference> refTargetsOldCellDependsOn = updater.getRelatedRefTargets(oldCell);
if (nodesOldCellDependsOn != null || refTargetsOldCellDependsOn != null) {
// Node was not changed, we have oldCell so it will not be re-created.
//
// Now all the dependencies of this (old) Cell should be added to currently active
// NodeReadAccessInEditorListener, so will be reported as parent Cell dependencies.
//
// Same logic is implemented in NodeReadAccessCasterInEditor.removeCellBuildNodeAccessListener(), so
// we should duplicate it here to emulate proper update process for parent cell.
NodeReadAccessInEditorListener parentReadAccessListener = NodeReadAccessCasterInEditor.getReadAccessListener();
if (parentReadAccessListener != null) {
if (nodesOldCellDependsOn != null) {
parentReadAccessListener.addNodesToDependOn(nodesOldCellDependsOn);
}
if (refTargetsOldCellDependsOn != null) {
parentReadAccessListener.addRefTargetsToDependOn(refTargetsOldCellDependsOn);
}
}
}
updater.getCurrentUpdateSession().reuseChildInfo(refContext);
return oldCell;
}
}
fillContextToCellMapForChildren(oldCell, childContextToCellMap = new HashMap<ReferencedNodeContext, EditorCell>());
updater.clearDependencies(oldCell);
}
try {
if (childContextToCellMap != null) {
myContextToOldCellMap.push(childContextToCellMap);
}
if (oldCell instanceof SynchronizeableEditorCell && ((SynchronizeableEditorCell) oldCell).canBeSynchronized() && isSynchronizable(node)) {
return syncEditorCell((SynchronizeableEditorCell) oldCell, refContext);
}
return createEditorCell_internal(myCreatingInspectedCell, refContext);
} finally {
if (childContextToCellMap != null) {
myContextToOldCellMap.pop();
}
}
} finally {
popTask();
}
}
/**
* In the current state we cannot synchronize nodes having property/link attributes.
* Example of not working synchronization is: property macro upon IntegerConstant in generator template.
* On adding/removing such macro synchronization logic cannot update editor properly and add/remove
* corresponding "wrapping" attribute cell..
* <p/>
* In future if proper way of handling such attributes is developed we can get rid of this method.
*/
private boolean isSynchronizable(SNode node) {
return !AttributeOperations.hasPropertyAttributes(node) && !AttributeOperations.hasLinkAttributes(node);
}
private boolean isNodeChanged(List<Pair<SNode, SNodeReference>> modifications, UpdaterImpl updater, EditorCell oldCell,
EditorCellContext cellContext) {
if (oldCell == null || oldCell.getCellContext() == null || cellContext.getHints().size() != oldCell.getCellContext().getHints().size() ||
!cellContext.getHints().containsAll(oldCell.getCellContext().getHints())) {
return true;
}
for (Pair<SNode, SNodeReference> modification : modifications) {
if (updater.isRelated(oldCell, modification)) {
return true;
}
}
return false;
}
public boolean isCreatingInspectedCell() {
return myCreatingInspectedCell;
}
private EditorCell syncEditorCell(SynchronizeableEditorCell editorCell, ReferencedNodeContext refContext) {
pushTask(getMessage(refContext, "+"));
EditorCell result = null;
try {
final SNode node = refContext.getNode();
NodeReadAccessInEditorListener nodeAccessListener = new NodeReadAccessInEditorListener();
try {
if (!isAttributedCell(editorCell, refContext)) {
editorCell = removeSideTransformHintCell(editorCell);
}
NodeReadAccessCasterInEditor.setCellBuildNodeReadAccessListener(nodeAccessListener);
editorCell.synchronize();
result = editorCell;
if (!isAttributedCell(result, refContext)) {
result = addSideTransformHintCell(result, node);
}
} catch (Throwable e) {
LOG.error("Failed to synchronize cell for node " + SNodeOperations.getDebugText(node), e);
result = new EditorCell_Error(getEditorContext(), node, "!exception!:" + SNodeOperations.getDebugText(node));
result.setBig(true);
result.setCellContext(getCellFactory().getCellContext());
} finally {
/**
* Always adding cell's node to the set of dependencies of the corresponding cell.
* It was done because read-access to the cell's node can be not recorded during
* editor update process for some specific editors - if cell's node was not required
* for the cell creation process.
*
* E.G.
* - node is represented by only constant cells
* - node is represented as a list of child nodes and at the moment we create editor
* there were no children in model
*
* "constant-only" cells should be still re-created if node attribute was added.
* "pure-child" cell should be re-created if first child was added to a node.
*
* To handle such situations & trigger editor update process for the corresponding
* cell, we are explicitly adding "self" node to the set of cell dependencies here.
*/
nodeAccessListener.nodeUnclassifiedReadAccess(node);
NodeReadAccessCasterInEditor.removeCellBuildNodeAccessListener();
addNodeDependenciesToEditor(result, nodeAccessListener);
if (!isAttributedCell(result, refContext)) {
result.putUserObject(BIG_CELL_CONTEXT, refContext);
EditorCell unwrappedNodeBigCell = getUnwrappedNodeBigCell(result, node);
if (unwrappedNodeBigCell != null) {
getUpdateSession().registerAsBigCell(unwrappedNodeBigCell);
}
}
}
return result;
} finally {
popTask();
}
}
private EditorCell createEditorCell_internal(boolean isInspectorCell, ReferencedNodeContext refContext) {
pushTask(getMessage(refContext, "+"));
final SNode node = refContext.getNode();
try {
//reset creating inspected cell : we don't create not-root inspected cells
myCreatingInspectedCell = false;
EditorCell nodeCell = null;
NodeReadAccessInEditorListener nodeAccessListener = new NodeReadAccessInEditorListener();
try {
NodeReadAccessCasterInEditor.setCellBuildNodeReadAccessListener(nodeAccessListener);
nodeCell = getCellFactory().createEditorCell(node, isInspectorCell);
if (!isAttributedCell(nodeCell, refContext)) {
nodeCell = addSideTransformHintCell(nodeCell, node);
}
} catch (Throwable e) {
LOG.error("Failed to create cell for node " + SNodeOperations.getDebugText(node), e);
nodeCell = new EditorCell_Error(getEditorContext(), node, "!exception!:" + SNodeOperations.getDebugText(node));
nodeCell.setBig(true);
nodeCell.setCellContext(getCellFactory().getCellContext());
} finally {
/**
* Always adding cell's node to the set of dependencies of the corresponding cell.
* It was done because read-access to the cell's node can be not recorded during
* editor update process for some specific editors - if cell's node was not required
* for the cell creation process.
*
* E.G.
* - node is represented by only constant cells
* - node is represented as a list of child nodes and at the moment we create editor
* there were no children in model
*
* "constant-only" cells should be still re-created if node attribute was added.
* "pure-child" cell should be re-created if first child was added to a node.
*
* To handle such situations & trigger editor update process for the corresponding
* cell, we are explicitly adding "self" node to the set of cell dependencies here.
*/
nodeAccessListener.nodeUnclassifiedReadAccess(node);
NodeReadAccessCasterInEditor.removeCellBuildNodeAccessListener();
assert nodeCell != null;
if (!isAttributedCell(nodeCell, refContext)) {
nodeCell.putUserObject(BIG_CELL_CONTEXT, refContext);
EditorCell unwrappedNodeBigCell = getUnwrappedNodeBigCell(nodeCell, node);
if (unwrappedNodeBigCell != null) {
getUpdateSession().registerAsBigCell(unwrappedNodeBigCell);
}
addNodeDependenciesToEditor(nodeCell, nodeAccessListener);
}
}
if (nodeCell instanceof EditorCell_Collection && ((EditorCell_Collection) nodeCell).canBeSynchronized() && !isSynchronizable(node)) {
((EditorCell_Collection) nodeCell).setCanBeSynchronized(false);
}
return nodeCell;
} finally {
popTask();
}
}
/**
* Property or reference attributes can wrap some particular cells of the main node editor into attribute node editor cells.
* Such "wrapping" cells when should be inserted into the main node editor instead of original property/reference cells.
* <p/>
* It can happen that main node editor contains only one property/reference cell. In such case if corresponding property/reference
* attribute is attached to the main node then the "main" cell will be wrapped into a property/reference attribute node editor cell(s)
* and returned from EditorCellFactory.createEditorCell() method execution.
* <p/>
* To properly handle such situations we should "unwrap" returned cell to get direct access to the big cell representing original main node.
* This method was created to handle such situations.
*
* @param cell EditorCell created by EditorCellFactory.createEditorCell() method
* @param node main node used as a parameter while creating this cell
* @return "big" cell representing main node. It will be either cell or it's child cell.
*/
private EditorCell getUnwrappedNodeBigCell(EditorCell cell, SNode node) {
SNode cellNode = cell.getSNode();
if (cellNode == node) {
return cell;
}
SConcept nodeConcept = cellNode.getConcept();
if (!nodeConcept.isSubConceptOf(SNodeUtil.concept_PropertyAttribute) &&
!nodeConcept.isSubConceptOf(SNodeUtil.concept_LinkAttribute)) {
// the only known possibility to get "wrapped" cell is when the cell is wrapped into a PropertyAttribute or LinkAttribute.
return cell;
}
Queue<EditorCell> cells = new LinkedList<EditorCell>();
cells.add(cell);
while (!cells.isEmpty()) {
EditorCell nextCell = cells.remove();
if (nextCell.getSNode() == node && !(nextCell instanceof EditorCell_STHint)) {
if (!nextCell.isBig()) {
// trying to avoid calling cell.getSNode().toString() for each node...
assert false :
"\"Not big\" cell found. Original cell: " + cell.getCellId() + ", node: " + cell.getSNode() + ", concept: " +
cell.getSNode().getConcept().getQualifiedName() + ". Found cell: " + nextCell.getCellId() + ", node: " +
node + ", concept: " + node.getConcept().getQualifiedName();
}
return nextCell;
}
if (nextCell instanceof EditorCell_Collection) {
for (EditorCell childCell : (EditorCell_Collection) nextCell) {
cells.add(childCell);
}
}
}
return null;
}
/**
* Some node attribute editors may return attributed node cell directly. (e.g. if specified editor is like: [> attributed node <]).
* For such editors we should skip all additional cell processing because additional cell processing was already performed for this
* cell while constructing it for the original node.
* <p/>
* This method is used to determine if the result of the generated attribute editor execution is equals to original cell of the
* attributed node.
*/
private boolean isAttributedCell(@NotNull EditorCell nodeCell, @NotNull ReferencedNodeContext refContext) {
return refContext.isNodeAttribute() && nodeCell.getSNode() != refContext.getNode();
}
private void addNodeDependenciesToEditor(EditorCell cell, NodeReadAccessInEditorListener listener) {
getUpdateSession().registerDependencies(cell, listener.getNodesToDependOn(), listener.getRefTargetsToDependOn());
for (Pair<SNodeReference, String> pair : listener.getDirtilyReadAccessedProperties()) {
getUpdateSession().registerDirtyDependency(cell, pair);
}
for (Pair<SNodeReference, String> pair : listener.getExistenceReadAccessProperties()) {
getUpdateSession().registerExistenceDependency(cell, pair);
}
}
private SynchronizeableEditorCell removeSideTransformHintCell(SynchronizeableEditorCell nodeCell) {
EditorCell_STHint hintCell = null;
// traversing all child cells of nodeCell representing same node and looking for EditorCell_STHint
Queue<EditorCell> queue = new LinkedList<EditorCell>();
queue.add(nodeCell);
while (hintCell == null && !queue.isEmpty()) {
EditorCell nextCell = queue.remove();
if (nextCell instanceof EditorCell_STHint) {
hintCell = (EditorCell_STHint) nextCell;
} else if (nextCell instanceof jetbrains.mps.openapi.editor.cells.EditorCell_Collection) {
for (EditorCell childCell : ((jetbrains.mps.openapi.editor.cells.EditorCell_Collection) nextCell)) {
if (childCell.getSNode() == nodeCell.getSNode()) {
queue.add(childCell);
}
}
}
}
return hintCell != null ? (SynchronizeableEditorCell) hintCell.uninstall() : nodeCell;
}
private EditorCell addSideTransformHintCell(EditorCell nodeCell, SNode node) {
Side side;
if (SideTransformInfoUtil.hasRightTransformInfo(node)) {
side = Side.RIGHT;
} else if (SideTransformInfoUtil.hasLeftTransformInfo(node)) {
side = Side.LEFT;
} else {
return nodeCell;
}
EditorCell unwrappedNodeBigCell = getUnwrappedNodeBigCell(nodeCell, node);
if (unwrappedNodeBigCell == null) {
return nodeCell;
}
String anchorId = SideTransformInfoUtil.getCellIdFromTransformInfo(node);
assert anchorId != null;
EditorCell anchorCell = CellFinderUtil.findChildById(unwrappedNodeBigCell, node, anchorId, true);
if (anchorCell == null) {
// anchor cell was not found. Possible reason: different node presentations in editor and inside inspector, so
// side-transforms in the main editor should not affect inspector.
return nodeCell;
}
assert
anchorCell.getSNode() == node :
"Anchor cell should be associated with the same node as main cell. Anchor cell node: " + anchorCell.getSNode().getNodeId() + "; main node: " +
node.getNodeId();
EditorCell_STHint sideTransformHintCell =
new EditorCell_STHint(unwrappedNodeBigCell, anchorCell, side, getCurrentlySelectedCellInfo(unwrappedNodeBigCell.getContext()));
return sideTransformHintCell.install();
}
private CellInfo getCurrentlySelectedCellInfo(EditorContext context) {
EditorCell selectedCell = context.getSelectedCell();
return selectedCell != null ? selectedCell.getCellInfo() : null;
}
private void pushTask(String message) {
jetbrains.mps.nodeEditor.EditorContext editorContextImpl = (jetbrains.mps.nodeEditor.EditorContext) getEditorContext();
if (editorContextImpl.isTracing()) {
editorContextImpl.pushTracerTask(message, true);
}
}
private void popTask() {
jetbrains.mps.nodeEditor.EditorContext editorContextImpl = (jetbrains.mps.nodeEditor.EditorContext) getEditorContext();
if (editorContextImpl.isTracing()) {
editorContextImpl.popTracerTask();
}
}
private String getMessage(ReferencedNodeContext refContext, String prefix) {
jetbrains.mps.nodeEditor.EditorContext editorContextImpl = (jetbrains.mps.nodeEditor.EditorContext) getEditorContext();
if (editorContextImpl.isTracing()) {
return prefix + refContext.toString();
}
return prefix;
}
}