/* * 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; } }