/* * Copyright 2003-2016 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.updater; import jetbrains.mps.ide.ThreadUtils; import jetbrains.mps.ide.project.ProjectHelper; import jetbrains.mps.logging.Logger; import jetbrains.mps.nodeEditor.EditorComponent; import jetbrains.mps.nodeEditor.cells.APICellAdapter; import jetbrains.mps.nodeEditor.commands.CommandContextImpl; import jetbrains.mps.nodeEditor.commands.CommandContextListener; import jetbrains.mps.openapi.editor.EditorComponentState; import jetbrains.mps.openapi.editor.EditorContext; import jetbrains.mps.openapi.editor.cells.EditorCell; import jetbrains.mps.openapi.editor.update.Updater; import jetbrains.mps.openapi.editor.update.UpdaterListener; import jetbrains.mps.project.Project; import jetbrains.mps.smodel.event.SModelEvent; import jetbrains.mps.typesystem.inference.TypeContextManager; import jetbrains.mps.util.Pair; import jetbrains.mps.util.WeakSet; import org.apache.log4j.LogManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeReference; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; /** * User: shatalin * Date: 03/09/14 */ public class UpdaterImpl implements Updater { private static final Logger LOG = Logger.wrap(LogManager.getLogger(UpdaterImpl.class)); @NotNull private final EditorComponent myEditorComponent; private UpdateSessionImpl myUpdateSession; private final UpdaterModelListenersController myModelListenersController; private List<UpdaterListener> myListeners = new ArrayList<>(); private Map<SNode, WeakReference<EditorCell>> myBigCellsMap = new WeakHashMap<>(); private Map<EditorCell, Set<SNode>> myRelatedNodes = new WeakHashMap<>(); private Map<EditorCell, Set<SNodeReference>> myRelatedRefTargets = new WeakHashMap<>(); private Map<Pair<SNodeReference, String>, WeakSet<EditorCell>> myCleanDependentCells = new HashMap<>(); private Map<Pair<SNodeReference, String>, WeakSet<EditorCell>> myDirtyDependentCells = new HashMap<>(); private Map<Pair<SNodeReference, String>, WeakSet<EditorCell>> myExistenceDependentCells = new HashMap<>(); private boolean myDisposed; private String[] myInitialHints; private Map<SNodeReference, Collection<String>> myEditorHintsForNodeMap = new HashMap<>(); private UpdateInfoIndex myUpdateInfoIndex; private boolean myInProgress; private boolean myProcessSelection; public UpdaterImpl(@NotNull EditorComponent editorComponent, CommandContextImpl commandContext) { myEditorComponent = editorComponent; commandContext.addListener(new CommandContextListenerImpl()); myModelListenersController = new UpdaterModelListenersController(this); } @Override public void update() { assert !myDisposed; boolean wasInProgress = fireEditorUpdateStarted(); doUpdate(null); myModelListenersController.clearCollectedEvents(); fireEditorUpdated(wasInProgress); } void update(List<SModelEvent> events) { boolean wasInProgress = fireEditorUpdateStarted(); doUpdate(events); fireEditorUpdated(wasInProgress); } private void doUpdate(List<SModelEvent> events) { if (!ThreadUtils.isInEDT()) { LOG.error("This method should be called in EDT", new Throwable()); } if (myDisposed) { return; } getEditorContext().getRepository().getModelAccess().checkReadAccess(); SNode editedNode = myEditorComponent.getEditedNode(); if (editedNode == null || editedNode.getModel() == null) { myEditorComponent.setRootCell(myEditorComponent.createEmptyCell()); } else { EditorComponentState state = getEditorContext().getEditorComponentState(); myEditorComponent.setRootCell(updateRootCell(editedNode, events)); getEditorContext().restoreEditorComponentState(state); } } private EditorCell updateRootCell(SNode node, List<SModelEvent> events) { assert !myDisposed; Project project = ProjectHelper.getProject(getEditorContext().getRepository()); assert project == null || !project.isDisposed() : "Update was executed for the editor associated with disposed project: " + project + ", editor: " + getEditorComponent() + ", node: " + getEditorComponent().getEditedNode(); assert myUpdateSession == null; myUpdateSession = createUpdateSession(node, events); EditorCell rootCell = null; try { Pair<EditorCell, UpdateInfoIndex> result = TypeContextManager.getInstance().runTypecheckingAction(myEditorComponent, () -> myUpdateSession.performUpdate()); rootCell = result.o1; myUpdateInfoIndex = result.o2; } finally { myUpdateSession = null; } myModelListenersController.attachListeners(node, getRelatedNodes(rootCell), getRelatedRefTargets(rootCell)); return rootCell; } @Override public UpdateSessionImpl getCurrentUpdateSession() { return myUpdateSession; } @Override public void flushModelEvents() { assert !myDisposed; if (!ThreadUtils.isInEDT()) { LOG.error("This method should be called in EDT", new Throwable()); } myModelListenersController.flush(); } @Override public void addListener(UpdaterListener listener) { myListeners.add(listener); } @Override public void removeListener(UpdaterListener listener) { myListeners.remove(listener); } @Override public boolean setInitialEditorHints(@Nullable String[] hints) { assert !myDisposed; boolean changed = !Arrays.equals(myInitialHints, hints); myInitialHints = hints; return changed; } @Nullable @Override public String[] getInitialEditorHints() { assert !myDisposed; if (myInitialHints == null) { return null; } String[] result = new String[myInitialHints.length]; System.arraycopy(myInitialHints, 0, result, 0, myInitialHints.length); return result; } @Override public void addExplicitEditorHintsForNode(SNodeReference nodeReference, String... hints) { Collection<String> currentHints = myEditorHintsForNodeMap.get(nodeReference); if (currentHints == null) { currentHints = new ArrayList<>(); Collections.addAll(currentHints, hints); myEditorHintsForNodeMap.put(nodeReference, currentHints); } else { Collections.addAll(currentHints, hints); } } @Override public void removeExplicitEditorHintsForNode(SNodeReference nodeReference, String... hints) { Collection<String> currentHints = myEditorHintsForNodeMap.get(nodeReference); if (currentHints != null) { for (String hint : hints) { currentHints.remove(hint); } if (currentHints.isEmpty()) { myEditorHintsForNodeMap.remove(nodeReference); } } } @Override public String[] getExplicitEditorHintsForNode(SNodeReference nodeReference) { Collection<String> hints = myEditorHintsForNodeMap.get(nodeReference); return hints == null ? null : hints.toArray(new String[hints.size()]); } @Override public void clearExplicitHints() { myEditorHintsForNodeMap.clear(); } private void fireCellSynchronized(EditorCell cell) { for (UpdaterListener nextListener : new ArrayList<>(myListeners)) { nextListener.cellSynchronizedWithModel(cell); } } private boolean fireEditorUpdateStarted() { if (myInProgress) { return true; } myInProgress = true; for (UpdaterListener nextListener : new ArrayList<>(myListeners)) { nextListener.editorUpdateStarted(myEditorComponent); } return false; } private void fireEditorUpdated(boolean wasInProgress) { if (wasInProgress) { return; } for (UpdaterListener nextListener : new ArrayList<>(myListeners)) { nextListener.editorUpdated(myEditorComponent); } myInProgress = false; } protected UpdateSessionImpl createUpdateSession(SNode node, List<SModelEvent> events) { UpdateSessionImpl result = new UpdateSessionImpl(node, events, this, myBigCellsMap, myRelatedNodes, myRelatedRefTargets, myCleanDependentCells, myDirtyDependentCells, myExistenceDependentCells, myUpdateInfoIndex); result.setInitialEditorHints(myInitialHints); result.setEditorHintsForNodeMap(myEditorHintsForNodeMap); // TODO: clean local state completely & use only info from this UpdateSessionImpl to update the editor after it. // myUpdateInfoIndex = null; return result; } @NotNull EditorContext getEditorContext() { return myEditorComponent.getEditorContext(); } private void clearCaches() { myBigCellsMap.clear(); myRelatedNodes.clear(); myRelatedRefTargets.clear(); myCleanDependentCells.clear(); myDirtyDependentCells.clear(); myExistenceDependentCells.clear(); } public void dispose() { myModelListenersController.dispose(); clearCaches(); myDisposed = true; } public EditorCell getBigCell(SNode node) { assert !myDisposed; WeakReference<EditorCell> editorCellWeakReference = myBigCellsMap.get(node); return editorCellWeakReference == null ? null : editorCellWeakReference.get(); } public void clearDependencies(EditorCell cell) { assert !myDisposed; myRelatedNodes.remove(cell); myRelatedRefTargets.remove(cell); } public Set<SNode> getRelatedNodes(EditorCell cell) { assert !myDisposed; Set<SNode> nodes = myRelatedNodes.get(cell); if (nodes == null) { return null; } return Collections.unmodifiableSet(nodes); } public Set<SNodeReference> getRelatedRefTargets(EditorCell cell) { assert !myDisposed; Set<SNodeReference> nodeProxies = myRelatedRefTargets.get(cell); if (nodeProxies == null) { return null; } return Collections.unmodifiableSet(nodeProxies); } public boolean isRelated(EditorCell cell, Pair<SNode, SNodeReference> modification) { assert !myDisposed; Set<SNode> sNodes = myRelatedNodes.get(cell); if (sNodes != null && sNodes.contains(modification.o1)) { return true; } Set<SNodeReference> refTargets = myRelatedRefTargets.get(cell); return refTargets != null && refTargets.contains(modification.o2); } /** * Used by UpdaterModelListener. Indicates if editor update should be executed as a * response for the specified property change, or it's sufficient to trigger EditorCells * synchronization. * <p/> * editor update should be triggered if: * - specified property was accessed "dirtily" while building this editor * - specified property was added/removed and corresponding property existence was checked * while building the editor * * @param propertyChange - pair of SNodeReference and property name modified * @param addedRemoved - indicates if property was newly added or removed from node * @return true if incremental editor update should be triggered */ boolean requiresUpdate(Pair<SNodeReference, String> propertyChange, boolean addedRemoved) { return myDirtyDependentCells.containsKey(propertyChange) || myExistenceDependentCells.containsKey(propertyChange) && addedRemoved; } /** * Performs EditorCells synchronization as a response to specified property change. * This method should be called only if requiresUpdate() returns false. * * @param propertyChange - pair of SNodeReference and property name modified */ void synchronizeCells(Pair<SNodeReference, String> propertyChange) { // TODO: do we need to synchronize myExistenceDependentCells cells at all? Iterable<EditorCell> cellsToSynchronize = myExistenceDependentCells.get(propertyChange); if (cellsToSynchronize != null) { for (EditorCell cell : cellsToSynchronize) { APICellAdapter.synchronizeViewWithModel(cell); fireCellSynchronized(cell); } } cellsToSynchronize = myCleanDependentCells.get(propertyChange); if (cellsToSynchronize != null) { for (EditorCell cell : cellsToSynchronize) { APICellAdapter.synchronizeViewWithModel(cell); fireCellSynchronized(cell); } } /* In the prev. version of the editor updater there was a code calling APICellAdapter.synchronizeViewWithModel(cell) for all editorCells within the same BigCell. This can be useful to revert any modifications performed by user, but not reflected within the model (just changing visual cell state e.g. modifying editable constant cell). I've removed this code because I did not find any reasons for this logic. If there will be use cases where it will be useful, corresponding code can be returned here. */ } boolean isSelectionProcessingAllowed() { return myProcessSelection; } @NotNull EditorComponent getEditorComponent() { return myEditorComponent; } UpdateInfoIndex getUpdateInfoIndex() { return myUpdateInfoIndex; } private class CommandContextListenerImpl extends CommandContextListener { @Override public void topLevelCommandStarted() { myProcessSelection = true; } @Override public void topLevelCommandFinished() { try { myModelListenersController.flush(); } finally { myProcessSelection = false; } } } }