/******************************************************************************* * Copyright (c) 2011, 2016 Wind River Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Wind River Systems - initial API and implementation * IBM Corporation - bug fixing *******************************************************************************/ package org.eclipse.debug.internal.ui.viewers.model; import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.debug.internal.ui.DebugUIPlugin; import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementCompareRequest; import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementMementoProvider; import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementMementoRequest; import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDelta; import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDeltaVisitor; import org.eclipse.debug.internal.ui.viewers.model.provisional.IStateUpdateListener; import org.eclipse.debug.internal.ui.viewers.model.provisional.ITreeModelViewer; import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdate; import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdateListener; import org.eclipse.debug.internal.ui.viewers.model.provisional.ModelDelta; import org.eclipse.jface.viewers.ITreeSelection; import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.ui.IMemento; import org.eclipse.ui.XMLMemento; /** * Class containing logic to save and restore expanded state of the tree model * viewer. *

* When the input to the viewer is changes, the tree model viewer attempts to * save the expansion state of elements as well as currently selected element and * scroll position. Each expanded element is queried for its memento and all the * collected mementos are saved into a delta tree then serialized by the viewer.
* When a new input is set to the viewer, the viewer compares the input's memento * with the stored mementos and if a match is found, it attempts to restore the * previous expansion state to the viewer. As elements are revealed and realized * in the viewer, the element's memento is compared against the memento stored in * the saved state delta. If matching elements are found in the delta, the expansion * and selection state is then restored to those elements. *

* Additionally to saving restoring state on input change, the viewer also * saves/restores elements' state when the model requests viewer to refresh model * structure. Since the viewer items are matched to the model elements using items' * indexes, inserting or removing elements in model can cause the expansion state * of elements to shift after a refresh. To compensate for this, the viewer saves * the elements before a refresh is performed into a delta, but without encoding * elements using mementos. As the refresh of the tree progresses, the save state * is restored to the tree and elements are expanded or collapsed as needed to * compensate for changes in model structure. *

* @see TreeModelContentProvider */ class ViewerStateTracker { // State update type constants used in notifying listeners static final int STATE_SAVE_SEQUENCE_BEGINS = 4; static final int STATE_SAVE_SEQUENCE_COMPLETE = 5; static final int STATE_RESTORE_SEQUENCE_BEGINS = 6; static final int STATE_RESTORE_SEQUENCE_COMPLETE = 7; /** * Dummy marker element used in the state delta. The marker indicates that a * given element in the pending state delta has been removed. It replaces * the original element so that it may optionally be garbage collected. */ private final static String ELEMENT_REMOVED = "ELEMENT_REMOVED"; //$NON-NLS-1$ /** * Collector of memento encoding requests. */ interface IElementMementoCollector { /** * Adds the request to this manager. * * @param request to add */ void addRequest(ElementMementoRequest request); /** * Notification the request is complete. * * @param request that was completed */ void requestComplete(ElementMementoRequest request); /** * Process the queued requests. Accepts no more new requests. */ void processReqeusts(); /** * Cancels the requests in progress. */ void cancel(); } /** * LRU cache for viewer states */ class LRUMap extends LinkedHashMap { private static final long serialVersionUID = 1L; private int fMaxSize; LRUMap(int maxSize) { super(); fMaxSize = maxSize; } @Override protected boolean removeEldestEntry(Entry eldest) { return size() > fMaxSize; } } /** * Content provider that is using this state tracker. */ private TreeModelContentProvider fContentProvider; ViewerStateTracker(TreeModelContentProvider contentProvider) { fContentProvider = contentProvider; } /** * Map of viewer states keyed by viewer input mementos */ private Map fViewerStates = new LRUMap<>(20); /** * Pending viewer state to be restored */ private ModelDelta fPendingState = null; /** * Flag indicating that the content provider is performing * state restore operations. */ private boolean fInStateRestore = false; /** * State update listeners */ private ListenerList fStateUpdateListeners = new ListenerList<>(); /** * Postpone restoring REVEAL element until the current updates are complete. * See bug 324100 */ protected PendingRevealDelta fPendingSetTopItem = null; /** * Set of IMementoManager's that are currently saving state */ private Set fPendingStateSaves = new HashSet<>(); /** * Used to queue a viewer input for state restore */ private Object fQueuedRestore = null; /** * Object used to key compare requests in map. */ private static class CompareRequestKey { CompareRequestKey(TreePath path, IModelDelta delta) { fPath = path; fDelta = delta; } TreePath fPath; IModelDelta fDelta; @Override public boolean equals(Object obj) { if (obj instanceof CompareRequestKey) { CompareRequestKey key = (CompareRequestKey) obj; return key.fDelta.equals(fDelta) && key.fPath.equals(fPath); } return false; } @Override public int hashCode() { return fDelta.hashCode() + fPath.hashCode(); } } /** * Compare requests that are currently running. */ private Map fCompareRequestsInProgress = new LinkedHashMap<>(); /** * Cancels pending updates. */ void dispose() { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); for (IElementMementoCollector emc : fPendingStateSaves) { emc.cancel(); } fStateUpdateListeners.clear(); for (ElementCompareRequest ecr : fCompareRequestsInProgress.values()) { ecr.cancel(); } fCompareRequestsInProgress.clear(); if (fPendingSetTopItem != null) { fPendingSetTopItem.dispose(); } } /** * Restores viewer state for the given input * * @param input * viewer input */ private void startRestoreViewerState(final Object input) { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); fPendingState = null; final IElementMementoProvider defaultProvider = ViewerAdapterService.getMementoProvider(input); if (defaultProvider != null) { // build a model delta representing expansion and selection state final ModelDelta delta = new ModelDelta(input, IModelDelta.NO_CHANGE); final XMLMemento inputMemento = XMLMemento.createWriteRoot("VIEWER_INPUT_MEMENTO"); //$NON-NLS-1$ final IElementMementoCollector manager = new IElementMementoCollector() { private IElementMementoRequest fRequest; /* * (non-Javadoc) * * @see * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers * . * IMementoManager#requestComplete(org.eclipse.debug.internal.ui * .viewers.model.provisional.IElementMementoRequest) */ @Override public void requestComplete(ElementMementoRequest request) { if (fContentProvider.isDisposed()) { return; } notifyStateUpdate(input, TreeModelContentProvider.UPDATE_COMPLETE, request); if (!request.isCanceled() && (request.getStatus() == null || request.getStatus().isOK())) { XMLMemento keyMemento = (XMLMemento) delta.getElement(); StringWriter writer = new StringWriter(); try { keyMemento.save(writer); final String keyMementoString = writer.toString(); ModelDelta stateDelta = fViewerStates.get(keyMementoString); if (stateDelta != null) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE RESTORE INPUT COMARE ENDED : " + fRequest + " - MATCHING STATE FOUND"); //$NON-NLS-1$ //$NON-NLS-2$ } // Process start of restore in an async cycle because we may still be inside inputChanged() // call. I.e. the "input.equals(fContentProvider.getViewer().getInput())" test may fail. fContentProvider.getViewer().getDisplay().asyncExec(new Runnable() { @Override public void run() { if (!fContentProvider.isDisposed() && input.equals(fContentProvider.getViewer().getInput())) { ModelDelta stateDelta2 = fViewerStates.remove(keyMementoString); if (stateDelta2 != null) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE RESTORE BEGINS"); //$NON-NLS-1$ DebugUIPlugin.trace("\tRESTORE: " + stateDelta2.toString()); //$NON-NLS-1$ notifyStateUpdate(input, STATE_RESTORE_SEQUENCE_BEGINS, null); } stateDelta2.setElement(input); fPendingState = stateDelta2; doInitialRestore(fPendingState); } } else { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE RESTORE CANCELED."); //$NON-NLS-1$ } } } }); } else { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE RESTORE INPUT COMARE ENDED : " + fRequest + " - NO MATCHING STATE"); //$NON-NLS-1$ //$NON-NLS-2$ } } } catch (IOException e) { DebugUIPlugin.log(e); } } else { notifyStateUpdate(input, STATE_RESTORE_SEQUENCE_BEGINS, null); } } /* * (non-Javadoc) * * @see * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers * .IMementoManager#processReqeusts() */ @Override public void processReqeusts() { notifyStateUpdate(input, STATE_RESTORE_SEQUENCE_BEGINS, null); if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE RESTORE INPUT COMARE BEGIN : " + fRequest); //$NON-NLS-1$ } notifyStateUpdate(input, TreeModelContentProvider.UPDATE_BEGINS, fRequest); defaultProvider.encodeElements(new IElementMementoRequest[] { fRequest }); } /* * (non-Javadoc) * * @see * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers * . * IMementoManager#addRequest(org.eclipse.debug.internal.ui.viewers * .model.provisional.IElementMementoRequest) */ @Override public void addRequest(ElementMementoRequest req) { fRequest = req; } @Override public void cancel() { // not used } }; manager.addRequest( new ElementMementoRequest(fContentProvider, fContentProvider.getViewer().getInput(), manager, delta.getElement(), fContentProvider.getViewerTreePath(delta), inputMemento, delta)); manager.processReqeusts(); } else { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE RESTORE: No input memento provider"); //$NON-NLS-1$ } } } /** * Appends the state of the given subtree to the current pending delta. * @param path Path to subtree to restore. */ void appendToPendingStateDelta(final TreePath path) { if (fContentProvider.getViewer() == null) { return; // Not initialized yet. } if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE APPEND BEGIN: " + path.getLastSegment()); //$NON-NLS-1$ } // build a model delta representing expansion and selection state final ModelDelta appendDeltaRoot = new ModelDelta(fContentProvider.getViewer().getInput(), IModelDelta.NO_CHANGE); ModelDelta delta = appendDeltaRoot; for (int i = 0; i < path.getSegmentCount(); i++) { delta = delta.addNode(path.getSegment(i), IModelDelta.NO_CHANGE); } if (!fContentProvider.getViewer().saveElementState(path, delta, IModelDelta.COLLAPSE | IModelDelta.EXPAND | IModelDelta.SELECT)) { // Path to save the state was not found or there was no // (expansion) state to save! Abort. if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE APPEND CANCEL: Element " + path.getLastSegment() + " not found."); //$NON-NLS-1$ //$NON-NLS-2$ } return; } // Append a marker CONTENT flag to all the delta nodes that contain the // EXPAND node. These // markers are used by the restore logic to know when a delta node can // be removed. delta.accept(new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta d, int depth) { if ((d.getFlags() & IModelDelta.EXPAND) != 0) { ((ModelDelta) d).setFlags(d.getFlags() | IModelDelta.CONTENT); } return true; } }); if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tAPPEND DELTA: " + appendDeltaRoot); //$NON-NLS-1$ } if (fPendingState != null) { // If the restore for the current input was never completed, // preserve // that restore along with the restore that was completed. if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tAPPEND OUTSTANDING RESTORE: " + fPendingState); //$NON-NLS-1$ } // If the append delta is generated for a sub-tree, copy the pending delta // attributes into the pending delta. if (path.getSegmentCount() > 0) { fPendingState.accept( new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta pendingDeltaNode, int depth) { TreePath pendingDeltaPath = fContentProvider.getViewerTreePath(pendingDeltaNode); if (path.startsWith(pendingDeltaPath, null)) { ModelDelta appendDelta = findDeltaForPath(appendDeltaRoot, pendingDeltaPath); appendDelta.setFlags(pendingDeltaNode.getFlags()); appendDelta.setChildCount(pendingDeltaNode.getChildCount()); appendDelta.setIndex(pendingDeltaNode.getIndex()); return true; } return false; } }); } // Copy the pending state into the new appended state. fPendingState.accept( new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta pendingDeltaNode, int depth) { // Skip the top element if (pendingDeltaNode.getParentDelta() == null) { return true; } // Find the node in the save delta which is the parent // of to the current pending delta node. // If the parent node cannot be found, it means that // most likely the user collapsed the parent node before // the children were ever expanded. // If the pending state node already exists in the parent // node, it is already processed and we can skip it. // If the pending state node does not contain any flags, // we can also skip it. ModelDelta saveDeltaNode = findSubDeltaParent(appendDeltaRoot, pendingDeltaNode); if (saveDeltaNode != null && !isDeltaInParent(pendingDeltaNode, saveDeltaNode) && pendingDeltaNode.getFlags() != IModelDelta.NO_CHANGE) { saveDeltaNode.setChildCount(pendingDeltaNode.getParentDelta().getChildCount()); copyIntoDelta(pendingDeltaNode, saveDeltaNode); } else { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tSKIPPED: " + pendingDeltaNode.getElement()); //$NON-NLS-1$ } } // If the pending delta node has a memento element, its // children should also be mementos therefore the copy // delta operation should have added all the children // of this pending delta node into the save delta. if (pendingDeltaNode.getElement() instanceof IMemento) { return false; } else { return pendingDeltaNode.getChildCount() > 0; } } }); } if (appendDeltaRoot.getChildDeltas().length > 0) { // Set the new delta root as the pending state delta. if (fPendingState == null) { notifyStateUpdate(appendDeltaRoot.getElement(), STATE_RESTORE_SEQUENCE_BEGINS, null); } fPendingState = appendDeltaRoot; if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE APPEND COMPLETE " + fPendingState); //$NON-NLS-1$ } } else { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE APPEND CANCELED: No Data"); //$NON-NLS-1$ } } } /** * Saves the viewer's state for the previous input. * @param oldInput * @param input the {@link ModelDelta} input */ protected void saveViewerState(Object input) { for (Iterator itr = fCompareRequestsInProgress.values().iterator(); itr.hasNext();) { itr.next().cancel(); itr.remove(); } IElementMementoProvider stateProvider = ViewerAdapterService.getMementoProvider(input); if (stateProvider != null) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE SAVE BEGIN: " + input); //$NON-NLS-1$ } // build a model delta representing expansion and selection state final ModelDelta saveDeltaRoot = new ModelDelta(input, IModelDelta.NO_CHANGE); buildViewerState(saveDeltaRoot); if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tSAVE DELTA FROM VIEW:\n" + saveDeltaRoot); //$NON-NLS-1$ } // check if pending restore reveal if (fPendingSetTopItem != null) { // set back the pending reveal flag ModelDelta revealDelta = fPendingSetTopItem.getDelta(); revealDelta.setFlags(revealDelta.getFlags() | IModelDelta.REVEAL); fPendingSetTopItem.dispose(); ModelDelta saveDeltaNode = findSubDeltaParent(saveDeltaRoot, revealDelta); if (saveDeltaNode != null) { clearRevealFlag(saveDeltaRoot); ModelDelta child = saveDeltaNode.getChildDelta(revealDelta.getElement(), revealDelta.getIndex()); if (child != null) { child.setFlags(child.getFlags() | IModelDelta.REVEAL); } else { // the node should be added if not found saveDeltaNode.setChildCount(revealDelta.getParentDelta().getChildCount()); copyIntoDelta(revealDelta, saveDeltaNode); } } else { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tSKIPPED: " + revealDelta.getElement()); //$NON-NLS-1$ } } } if (fPendingState != null) { // If the restore for the current input was never completed, // preserve // that restore along with the restore that was completed. if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tSAVE OUTSTANDING RESTORE: " + fPendingState); //$NON-NLS-1$ } IModelDeltaVisitor pendingStateVisitor = new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta pendingDeltaNode, int depth) { // Ignore the top element. if (pendingDeltaNode.getParentDelta() == null) { return true; } // Find the node in the save delta which is the parent // of to the current pending delta node. // If the parent node cannot be found, it means that // most likely the user collapsed the parent node before // the children were ever expanded. // If the pending state node already exists in the // parent // node, it is already processed and we can skip it. // If the pending state node does not contain any flags, // we can also skip it. ModelDelta saveDeltaNode = findSubDeltaParent(saveDeltaRoot, pendingDeltaNode); if (saveDeltaNode != null && !isDeltaInParent(pendingDeltaNode, saveDeltaNode) && pendingDeltaNode.getFlags() != IModelDelta.NO_CHANGE) { // There should be only one delta element with // the REVEAL flag in the entire save delta. The // reveal flag in the pending delta trumps the one // in the save delta because most likely the restore // operation did not yet complete the reveal // operation. if ((pendingDeltaNode.getFlags() & IModelDelta.REVEAL) != 0) { clearRevealFlag(saveDeltaRoot); } saveDeltaNode.setChildCount(pendingDeltaNode.getParentDelta().getChildCount()); copyIntoDelta(pendingDeltaNode, saveDeltaNode); } else { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tSKIPPED: " + pendingDeltaNode.getElement()); //$NON-NLS-1$ } } // If the pending delta node has a memento element, its // children should also be mementos therefore the copy // delta operation should have added all the children // of this pending delta node into the save delta. if (pendingDeltaNode.getElement() instanceof IMemento) { return false; } else { return pendingDeltaNode.getChildCount() > 0; } } }; fPendingState.accept(pendingStateVisitor); } if (saveDeltaRoot.getChildDeltas().length > 0) { // encode delta with mementos in place of elements, in non-UI // thread encodeDelta(saveDeltaRoot, stateProvider); } else { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE SAVE CANCELED, NO DATA"); //$NON-NLS-1$ } } } } private void clearRevealFlag(ModelDelta saveRootDelta) { IModelDeltaVisitor clearDeltaVisitor = new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta delta, int depth) { if ((delta.getFlags() & IModelDelta.REVEAL) != 0) { ((ModelDelta) delta).setFlags(delta.getFlags() & ~IModelDelta.REVEAL); } return true; } }; saveRootDelta.accept(clearDeltaVisitor); } private ModelDelta findSubDeltaParent(ModelDelta destinationDeltaRoot, IModelDelta subDelta) { // Create a path of elements to the sub-delta. LinkedList deltaPath = new LinkedList<>(); IModelDelta delta = subDelta; while (delta.getParentDelta() != null) { delta = delta.getParentDelta(); deltaPath.addFirst(delta); } // For each element in the path of the sub-delta, find the corresponding // element in the destination delta Iterator itr = deltaPath.iterator(); // Skip the root element itr.next(); ModelDelta saveDelta = destinationDeltaRoot; while (saveDelta != null && itr.hasNext()) { IModelDelta itrDelta = itr.next(); saveDelta = saveDelta.getChildDelta(itrDelta.getElement(), itrDelta.getIndex()); } return saveDelta; } private ModelDelta findDeltaForPath(ModelDelta root, TreePath path) { ModelDelta delta = root; for (int i = 0; i < path.getSegmentCount(); i++) { delta = delta.getChildDelta(path.getSegment(i)); if (delta == null) { return null; } } return delta; } private boolean isDeltaInParent(IModelDelta delta, ModelDelta destParent) { return destParent.getChildDelta(delta.getElement(), delta.getIndex()) != null; } private void copyIntoDelta(IModelDelta delta, ModelDelta destParent) { ModelDelta newDelta = destParent.addNode(delta.getElement(), delta.getIndex(), delta.getFlags(), delta .getChildCount()); for (int i = 0; i < delta.getChildDeltas().length; i++) { copyIntoDelta(delta.getChildDeltas()[i], newDelta); } } /** * Encodes delta elements into mementos using the given provider. * @param rootDelta the {@link ModelDelta} to encode * @param defaultProvider the default provider to use when processing the given delta * */ protected void encodeDelta(final ModelDelta rootDelta, final IElementMementoProvider defaultProvider) { final Object input = rootDelta.getElement(); final XMLMemento inputMemento = XMLMemento.createWriteRoot("VIEWER_INPUT_MEMENTO"); //$NON-NLS-1$ final XMLMemento childrenMemento = XMLMemento.createWriteRoot("CHILDREN_MEMENTO"); //$NON-NLS-1$ final IElementMementoCollector manager = new IElementMementoCollector() { /** * list of memento fRequests */ private List fRequests = new ArrayList<>(); /** * Flag indicating whether the encoding of delta has been canceled. */ private boolean fCanceled = false; /* * (non-Javadoc) * * @see * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers * .IMementoManager * #requestComplete(org.eclipse.debug.internal.ui.viewers * .model.provisional.IElementMementoRequest) */ @Override public void requestComplete(ElementMementoRequest request) { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); notifyStateUpdate(input, TreeModelContentProvider.UPDATE_COMPLETE, request); if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tSTATE END: " + request); //$NON-NLS-1$ } if (!request.isCanceled() && (request.getStatus() == null || request.getStatus().isOK())) { boolean requestsComplted = false; if (!fCanceled) { fRequests.remove(request); requestsComplted = fRequests.isEmpty(); } if (requestsComplted) { XMLMemento keyMemento = (XMLMemento) rootDelta.getElement(); StringWriter writer = new StringWriter(); try { keyMemento.save(writer); fViewerStates.put(writer.toString(), rootDelta); } catch (IOException e) { DebugUIPlugin.log(e); } if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE SAVE COMPLETED: " + rootDelta); //$NON-NLS-1$ } stateSaveComplete(input, this); } } else { cancel(); } } @Override public void cancel() { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); if (fCanceled) { return; } fCanceled = true; for (IElementMementoRequest req : fRequests) { req.cancel(); } fRequests.clear(); if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE SAVE ABORTED: " + rootDelta.getElement()); //$NON-NLS-1$ } stateSaveComplete(input, this); } /* * (non-Javadoc) * * @see * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers * .IMementoManager#processReqeusts() */ @Override public void processReqeusts() { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); Map> providers = new HashMap<>(); for (IElementMementoRequest request : fRequests) { notifyStateUpdate(input, TreeModelContentProvider.UPDATE_BEGINS, request); IElementMementoProvider provider = ViewerAdapterService.getMementoProvider(request.getElement()); if (provider == null) { provider = defaultProvider; } List reqs = providers.get(provider); if (reqs == null) { reqs = new ArrayList<>(); providers.put(provider, reqs); } reqs.add(request); } for (Entry> entry : providers.entrySet()) { IElementMementoProvider provider = entry.getKey(); List reqs = entry.getValue(); provider.encodeElements(reqs.toArray(new IElementMementoRequest[reqs.size()])); } } /* * (non-Javadoc) * * @see * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers * .IMementoManager * #addRequest(org.eclipse.debug.internal.ui.viewers. * model.provisional.IElementMementoRequest) */ @Override public void addRequest(ElementMementoRequest request) { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); fRequests.add(request); } }; IModelDeltaVisitor visitor = new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta delta, int depth) { // Add the CONTENT flag to all nodes with an EXPAND flag. // During restoring, this flag is used as a marker indicating // whether all the content of a given element has been // retrieved. if ((delta.getFlags() | IModelDelta.EXPAND) != 0) { ((ModelDelta)delta).setFlags(delta.getFlags() | IModelDelta.CONTENT); } // This is the root element, save the root element memento in 'inputMemento'. if (delta.getParentDelta() == null) { manager.addRequest(new ElementMementoRequest(fContentProvider, input, manager, delta.getElement(), fContentProvider.getViewerTreePath(delta), inputMemento, (ModelDelta) delta)); } else { // If this is another node element, save the memento to a children memento. if (!(delta.getElement() instanceof XMLMemento)) { manager.addRequest(new ElementMementoRequest(fContentProvider, input, manager, delta.getElement(), fContentProvider.getViewerTreePath(delta), childrenMemento .createChild("CHILD_ELEMENT"), (ModelDelta) delta)); //$NON-NLS-1$ } } return true; } }; rootDelta.accept(visitor); stateSaveStarted(input, manager); manager.processReqeusts(); } /** * Called when a state save is starting. * @param input the {@link ModelDelta} input * @param manager the manager to notify */ private void stateSaveStarted(Object input, IElementMementoCollector manager) { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); notifyStateUpdate(input, STATE_SAVE_SEQUENCE_BEGINS, null); fPendingStateSaves.add(manager); } /** * Called when a state save is complete. * @param input the {@link ModelDelta} input * @param manager the manager to notify */ private void stateSaveComplete(Object input, IElementMementoCollector manager) { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); notifyStateUpdate(input, STATE_SAVE_SEQUENCE_COMPLETE, null); fPendingStateSaves.remove(manager); if (fQueuedRestore != null) { Object temp = fQueuedRestore; fQueuedRestore = null; restoreViewerState(temp); } } /** * Returns whether any state saving is in progress. * * @return whether any state saving is in progress */ private boolean isSavingState() { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); return !fPendingStateSaves.isEmpty(); } /** * Restores the viewer state unless a save is taking place. If a save is * taking place, the restore is queued. * * @param input * viewer input */ protected void restoreViewerState(final Object input) { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); fPendingState = null; if (isSavingState()) { fQueuedRestore = input; } else { startRestoreViewerState(input); } } public void cancelRestore(final TreePath path, final int flags) { if (fInStateRestore) { // If we are currently processing pending state already, ignore // cancelRestore requests. These requests may be triggered in the viewer // by changes to the tree state (Bug 295585). return; } if ((flags & IModelDelta.REVEAL) != 0 && fPendingSetTopItem != null) { fPendingSetTopItem.dispose(); return; } // Nothing else to do if (fPendingState == null) { return; } if ((flags & (IModelDelta.SELECT | IModelDelta.REVEAL)) != 0) { // If we're canceling reveal and this is waiting for updates to complete // then just cancel it and return // If we're canceling select or reveal, cancel it for all of pending deltas final int mask = flags & (IModelDelta.SELECT | IModelDelta.REVEAL); fPendingState.accept(new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta delta, int depth) { int deltaFlags = delta.getFlags(); int newFlags = deltaFlags & ~mask; if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { if (deltaFlags != newFlags) { DebugUIPlugin.trace("\tCANCEL: " + delta.getElement() + "(" + Integer.toHexString(deltaFlags & mask) + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } ((ModelDelta)delta).setFlags(newFlags); return true; } }); } if ((flags & ~(IModelDelta.SELECT | IModelDelta.REVEAL)) != 0) { final int mask = flags & ~(IModelDelta.SELECT | IModelDelta.REVEAL); // For other flags (EXPAND/COLLAPSE), cancel only from the matching path. fPendingState.accept(new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta delta, int depth) { if (depth < path.getSegmentCount()) { // Descend until we reach a matching depth. TreePath deltaPath = fContentProvider.getViewerTreePath(delta); if (path.startsWith(deltaPath, null)) { return true; } else { return false; } } else if (depth == path.getSegmentCount()) { TreePath deltaPath = fContentProvider.getViewerTreePath(delta); if (deltaPath.equals(path)) { int deltaFlags = delta.getFlags(); int newFlags = deltaFlags & ~mask; if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { if (deltaFlags != newFlags) { DebugUIPlugin.trace("\tCANCEL: " + delta.getElement() + "(" + Integer.toHexString(deltaFlags & mask) + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } ((ModelDelta)delta).setFlags(newFlags); if ((flags & IModelDelta.EXPAND) != 0) { // Descend delta to clear the EXPAND flags of a canceled expand return true; } } return false; } else { // We're clearing out flags of a matching sub-tree // assert (flags & IModelDelta.EXPAND) != 0; if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { if (delta.getFlags() != IModelDelta.NO_CHANGE) { DebugUIPlugin.trace("\tCANCEL: " + delta.getElement() + "(" + Integer.toHexString(delta.getFlags()) + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } ((ModelDelta)delta).setFlags(IModelDelta.NO_CHANGE); return true; } } }); } } /** * Perform any restoration required for the given tree path. *

* This method is called after every viewer update completion to continue * restoring the expansion state that was previously saved. * * @param path the tree path to update * @param modelIndex the index in the current model * @param knowsHasChildren if the content provider knows it has children already * @param knowsChildCount if the content provider knows the current child count already * @param checkChildrenRealized if any realized children should be checked or not */ void restorePendingStateOnUpdate(final TreePath path, final int modelIndex, final boolean knowsHasChildren, final boolean knowsChildCount, final boolean checkChildrenRealized) { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); if (fPendingState == null) { return; } IModelDeltaVisitor visitor = new IModelDeltaVisitor() { @Override public boolean visit(final IModelDelta delta, int depth) { Object element = delta.getElement(); Object potentialMatch = depth != 0 ? path.getSegment(depth - 1) : fContentProvider.getViewer().getInput(); // Only process if the depth in the delta matches the tree path. if (depth == path.getSegmentCount()) { if (element instanceof IMemento) { IElementMementoProvider provider = ViewerAdapterService.getMementoProvider(potentialMatch); if (provider == null) { provider = ViewerAdapterService.getMementoProvider(fContentProvider.getViewer().getInput()); } if (provider != null) { CompareRequestKey key = new CompareRequestKey(path, delta); ElementCompareRequest existingRequest = fCompareRequestsInProgress .get(key); if (existingRequest != null) { // Check all the running compare updates for a // matching tree path. // If found, just update the flags. existingRequest.setKnowsHasChildren(knowsHasChildren); existingRequest.setKnowsChildCount(knowsChildCount); existingRequest.setCheckChildrenRealized(checkChildrenRealized); } else { // Start a new compare request ElementCompareRequest compareRequest = new ElementCompareRequest( fContentProvider, fContentProvider.getViewer().getInput(), potentialMatch, path, (IMemento) element, (ModelDelta) delta, modelIndex, knowsHasChildren, knowsChildCount, checkChildrenRealized); fCompareRequestsInProgress.put(key, compareRequest); if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tSTATE BEGIN: " + compareRequest); //$NON-NLS-1$ } notifyStateUpdate(element, TreeModelContentProvider.UPDATE_BEGINS, compareRequest); provider.compareElements(new IElementCompareRequest[] { compareRequest }); } } } else if (element.equals(potentialMatch)) { // Element comparison already succeeded, and it matches // our element. // Call restore with delta to process the delta flags. restorePendingStateNode((ModelDelta) delta, knowsHasChildren, knowsChildCount, checkChildrenRealized); } return false; } // Only follow the paths that match the delta. return element.equals(potentialMatch); } }; try { fInStateRestore = true; fPendingState.accept(visitor); } finally { fInStateRestore = false; } checkIfRestoreComplete(); } /** * Checks whether restoring pending state is already complete. */ void checkIfRestoreComplete() { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); if (fPendingState == null) { return; } /** * Used to determine when restoration delta has been processed */ class CheckState implements IModelDeltaVisitor { private boolean complete = true; /* * (non-Javadoc) * * @see * org.eclipse.debug.internal.ui.viewers.provisional.IModelDeltaVisitor * #visit(org.eclipse.debug.internal.ui.viewers.provisional.IModelDelta, * int) */ @Override public boolean visit(IModelDelta delta, int depth) { // Filter out the CONTENT flags from the delta flags, the content // flag is only used as a marker indicating that all the sub-elements // of a given delta have been retrieved. int flags = (delta.getFlags() & ~IModelDelta.CONTENT); if (flags != IModelDelta.NO_CHANGE) { IModelDelta parentDelta = delta.getParentDelta(); // Remove the delta if : // - The parent delta has no more flags on it (the content flag is removed as well), // which means that parent element's children have been completely exposed. // - There are no more pending updates for the element. // - If element is a memento, there are no state requests pending. if (parentDelta != null && parentDelta.getFlags() == IModelDelta.NO_CHANGE) { TreePath deltaPath = fContentProvider.getViewerTreePath(delta); if ( !fContentProvider.areElementUpdatesPending(deltaPath) && (!(delta.getElement() instanceof IMemento) || !areMementoUpdatesPending(delta)) ) { removeDelta(delta); return false; } } if (flags != IModelDelta.REVEAL || (delta.getElement() instanceof IMemento)) { complete = false; return false; } } return true; } public boolean isComplete() { return complete; } private boolean areMementoUpdatesPending(IModelDelta delta) { for (CompareRequestKey key : fCompareRequestsInProgress.keySet()) { if (delta.getElement().equals(key.fDelta.getElement())) { return true; } } return false; } private void removeDelta(IModelDelta delta) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tRESTORE REMOVED: " + delta.getElement()); //$NON-NLS-1$ } delta.accept(new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta _visitorDelta, int depth) { ModelDelta visitorDelta = (ModelDelta) _visitorDelta; visitorDelta.setElement(ELEMENT_REMOVED); visitorDelta.setFlags(IModelDelta.NO_CHANGE); return true; } }); } } CheckState state = new CheckState(); fPendingState.accept(state); if (state.isComplete()) { // notify restore complete if REVEAL was restored also, otherwise // postpone until then. if (fPendingSetTopItem == null) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE RESTORE COMPELTE: " + fPendingState); //$NON-NLS-1$ } notifyStateUpdate(fPendingState.getElement(), STATE_RESTORE_SEQUENCE_COMPLETE, null); } fPendingState = null; } } /** * Restores the pending state in the given delta node. This method is called * once the state tracker has found the element which matches the element in * the given delta node. * @param delta the {@link ModelDelta} to restore from * @param knowsHasChildren if the content provider has computed its children * @param knowsChildCount if the content provider has already computed the child count * @param checkChildrenRealized if any realized children should be checked */ void restorePendingStateNode(final ModelDelta delta, boolean knowsHasChildren, boolean knowsChildCount, boolean checkChildrenRealized) { final TreePath treePath = fContentProvider.getViewerTreePath(delta); final IInternalTreeModelViewer viewer = fContentProvider.getViewer(); // Attempt to expand the node only if the children are known. if (knowsHasChildren) { if ((delta.getFlags() & IModelDelta.EXPAND) != 0) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tRESTORE EXPAND: " + treePath.getLastSegment()); //$NON-NLS-1$ } viewer.expandToLevel(treePath, 1); delta.setFlags(delta.getFlags() & ~IModelDelta.EXPAND); } if ((delta.getFlags() & IModelDelta.COLLAPSE) != 0) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tRESTORE COLLAPSE: " + treePath.getLastSegment()); //$NON-NLS-1$ } // Check auto-expand before collapsing an element (bug 335734) int autoexpand = fContentProvider.getViewer().getAutoExpandLevel(); if (autoexpand != ITreeModelViewer.ALL_LEVELS && autoexpand < (treePath.getSegmentCount() + 1)) { fContentProvider.getViewer().setExpandedState(treePath, false); } delta.setFlags(delta.getFlags() & ~IModelDelta.COLLAPSE); } } if ((delta.getFlags() & IModelDelta.SELECT) != 0) { delta.setFlags(delta.getFlags() & ~IModelDelta.SELECT); if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tRESTORE SELECT: " + treePath.getLastSegment()); //$NON-NLS-1$ } ITreeSelection currentSelection = (ITreeSelection)viewer.getSelection(); if (currentSelection == null || currentSelection.isEmpty()) { viewer.setSelection(new TreeSelection(treePath), false, false); } else { TreePath[] currentPaths = currentSelection.getPaths(); boolean pathInSelection = false; for (int i = 0; i < currentPaths.length; i++) { if (currentPaths[i].equals(treePath)) { pathInSelection = true; break; } } // Only set the selection if the element is not yet in // selection. Otherwise the setSelection() call will // update selection listeners needlessly. if (!pathInSelection) { TreePath[] newPaths = new TreePath[currentPaths.length + 1]; System.arraycopy(currentPaths, 0, newPaths, 0, currentPaths.length); newPaths[newPaths.length - 1] = treePath; viewer.setSelection(new TreeSelection(newPaths), false, false); } } } if ((delta.getFlags() & IModelDelta.REVEAL) != 0) { delta.setFlags(delta.getFlags() & ~IModelDelta.REVEAL); // Look for the reveal flag in the child deltas. If // A child delta has the reveal flag, do not set the // top element yet. boolean setTopItem = true; IModelDelta[] childDeltas = delta.getChildDeltas(); for (int i = 0; i < childDeltas.length; i++) { IModelDelta childDelta = childDeltas[i]; int modelIndex = childDelta.getIndex(); if (modelIndex >= 0 && (childDelta.getFlags() & IModelDelta.REVEAL) != 0) { setTopItem = false; } } if (setTopItem) { Assert.isTrue(fPendingSetTopItem == null); fPendingSetTopItem = new PendingRevealDelta(treePath, delta); viewer.addViewerUpdateListener(fPendingSetTopItem); } } // If we know the child count of the element, look for the reveal // flag in the child deltas. For the children with reveal flag start // a new update. // If the child delta's index is out of range, strip the reveal flag // since it is no longer applicable. if (knowsChildCount) { int childCount = viewer.getChildCount(treePath); if (childCount >= 0) { ModelDelta[] childDeltas = (ModelDelta[])delta.getChildDeltas(); for (int i = 0; i < childDeltas.length; i++) { ModelDelta childDelta = childDeltas[i]; int modelIndex = childDelta.getIndex(); if (modelIndex >= 0 && (childDelta.getFlags() & IModelDelta.REVEAL) != 0) { if (modelIndex < childCount) { fContentProvider.doUpdateElement(treePath, modelIndex); } else { childDelta.setFlags(childDelta.getFlags() & ~IModelDelta.REVEAL); } } } } } // Some children of this element were just updated. If all its // children are now realized, clear out any elements that still // have flags, because they represent elements that were removed. if ((checkChildrenRealized && !fContentProvider.areChildrenUpdatesPending(treePath) && fContentProvider.getViewer().getElementChildrenRealized(treePath)) || (knowsHasChildren && !viewer.getHasChildren(treePath)) ) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tRESTORE CONTENT: " + treePath.getLastSegment()); //$NON-NLS-1$ } delta.setFlags(delta.getFlags() & ~IModelDelta.CONTENT); } } /** * Utility that reveals the saved top item in the viewer. It listens for * all content updates to complete in order to avoid having the desired top item * scroll out as view content is filled in. *
* Revealing some elements can trigger expanding some of elements * that have been just revealed. Therefore, we have to check one * more time after the new triggered updates are completed if we * have to set again the top index */ private class PendingRevealDelta implements IViewerUpdateListener { private final TreePath fPathToReveal; private final ModelDelta fRevealDelta; PendingRevealDelta(TreePath pathToReveal, ModelDelta revealDelta) { fPathToReveal = pathToReveal; fRevealDelta = revealDelta; } /** * Counter that tracks how many time the viewer updates were completed. */ private int fCounter = 0; private Object fModelInput = fPendingState.getElement(); @Override public void viewerUpdatesComplete() { Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() ); IInternalTreeModelViewer viewer = fContentProvider.getViewer(); if (viewer == null || fPendingSetTopItem != this) { return; } TreePath topPath = viewer.getTopElementPath(); if (!fPathToReveal.equals(topPath)) { TreePath parentPath = fPathToReveal.getParentPath(); int index = viewer.findElementIndex(parentPath, fPathToReveal.getLastSegment()); if (index >= 0) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tRESTORE REVEAL: " + fPathToReveal.getLastSegment()); //$NON-NLS-1$ } viewer.reveal(parentPath, index); } } fCounter++; // in case the pending state was already set to null, we assume that // all others elements are restored, so we don't expect that REVEAL will // trigger other updates if (fCounter > 1 || fPendingState == null) { dispose(); } } @Override public void viewerUpdatesBegin() {} @Override public void updateStarted(IViewerUpdate update) {} @Override public void updateComplete(IViewerUpdate update) {} /** * Returns delta that is used to reveal the item. * @return delta to be revealed. */ public ModelDelta getDelta() { return fRevealDelta; } /** * Resets the item */ public void dispose() { // top item is set fPendingSetTopItem = null; IInternalTreeModelViewer viewer = fContentProvider.getViewer(); if (viewer == null) { return; } // remove myself as viewer update listener viewer.removeViewerUpdateListener(this); if (fPendingState == null) { if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("STATE RESTORE COMPELTE: " + fPendingState); //$NON-NLS-1$ } notifyStateUpdate(fModelInput, STATE_RESTORE_SEQUENCE_COMPLETE, null); } else { checkIfRestoreComplete(); } } } /** * Restore selection/expansion based on items already in the viewer * @param delta the {@link ModelDelta} to restore from */ protected void doInitialRestore(ModelDelta delta) { // Find the reveal delta and mark nodes on its path // to reveal as elements are updated. markRevealDelta(delta); // Restore visible items. // Note (Pawel Piech): the initial list of items is normally // empty, so in most cases the code below does not do anything. // Instead doRestore() is called when various updates complete. int count = fContentProvider.getViewer().getChildCount(TreePath.EMPTY); for (int i = 0; i < count; i++) { Object data = fContentProvider.getViewer().getChildElement(TreePath.EMPTY, i); if (data != null) { restorePendingStateOnUpdate(new TreePath(new Object[]{data}), i, false, false, false); } } } /** * Finds the delta with the reveal flag, then it walks up this * delta and marks all the parents of it with the reveal flag. * These flags are then used by the restore logic to restore * and reveal all the nodes leading up to the element that should * be ultimately at the top. * @param rootDelta Delta to search * @return The node just under the rootDelta which contains * the reveal flag. null if no reveal flag was found. */ private ModelDelta markRevealDelta(ModelDelta rootDelta) { final ModelDelta[] revealDelta = new ModelDelta[1]; IModelDeltaVisitor visitor = new IModelDeltaVisitor() { @Override public boolean visit(IModelDelta delta, int depth) { if ( (delta.getFlags() & IModelDelta.REVEAL) != 0) { revealDelta[0] = (ModelDelta)delta; } // Keep recursing only if we haven't found our delta yet. return revealDelta[0] == null; } }; rootDelta.accept(visitor); if (revealDelta[0] != null) { ModelDelta parentDelta = (ModelDelta)revealDelta[0].getParentDelta(); while(parentDelta.getParentDelta() != null) { revealDelta[0] = parentDelta; revealDelta[0].setFlags(revealDelta[0].getFlags() | IModelDelta.REVEAL); parentDelta = (ModelDelta)parentDelta.getParentDelta(); } } return revealDelta[0]; } /** * Builds a delta with the given root delta for expansion/selection state. * * @param delta * root delta */ private void buildViewerState(ModelDelta delta) { IInternalTreeModelViewer viewer = fContentProvider.getViewer(); viewer.saveElementState(TreeModelContentProvider.EMPTY_TREE_PATH, delta, IModelDelta.SELECT | IModelDelta.EXPAND); // Add memento for top item if it is mapped to an element. The reveal memento // is in its own path to avoid requesting unnecessary data when restoring it. TreePath topElementPath = viewer.getTopElementPath(); if (topElementPath != null) { ModelDelta parentDelta = delta; TreePath parentPath = TreeModelContentProvider.EMPTY_TREE_PATH; for (int i = 0; i < topElementPath.getSegmentCount(); i++) { Object element = topElementPath.getSegment(i); int index = viewer.findElementIndex(parentPath, element); ModelDelta childDelta = parentDelta.getChildDelta(element); if (childDelta == null) { parentDelta = parentDelta.addNode(element, index, IModelDelta.NO_CHANGE); } else { parentDelta = childDelta; } parentPath = parentPath.createChildPath(element); } parentDelta.setFlags(parentDelta.getFlags() | IModelDelta.REVEAL); } } /** * Cancels any outstanding compare requests for given element and its children. * @param path Path of element to cancel updates for. */ void cancelStateSubtreeUpdates(TreePath path) { for (Iterator itr = fCompareRequestsInProgress.keySet().iterator(); itr.hasNext();) { CompareRequestKey key = itr.next(); if (key.fPath.startsWith(path, null)) { ElementCompareRequest compareRequest = fCompareRequestsInProgress.get(key); compareRequest.cancel(); itr.remove(); } } } void compareFinished(ElementCompareRequest request, ModelDelta delta) { notifyStateUpdate(request.getViewerInput(), TreeModelContentProvider.UPDATE_COMPLETE, request); if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) { DebugUIPlugin.trace("\tSTATE END: " + request + " = " + false); //$NON-NLS-1$ //$NON-NLS-2$ } fCompareRequestsInProgress.remove(new CompareRequestKey(request.getElementPath(), delta)); if (!request.isCanceled()) { if (request.isEqual()) { delta.setElement(request.getElement()); restorePendingStateNode(delta, request.knowsHasChildren(), request.knowChildCount(), request.checkChildrenRealized()); } else if (request.getModelIndex() != -1) { // Comparison failed. // Check if the delta has a reveal flag, and if its index // matches the index of the element that it was compared // against. If this is the case, strip the reveal flag from // the delta as it is most likely not applicable anymore. if ((delta.getFlags() & IModelDelta.REVEAL) != 0 && delta.getIndex() == request.getModelIndex()) { delta.setFlags(delta.getFlags() & ~IModelDelta.REVEAL); } } } checkIfRestoreComplete(); } void addStateUpdateListener(IStateUpdateListener listener) { fStateUpdateListeners.add(listener); } void removeStateUpdateListener(IStateUpdateListener listener) { fStateUpdateListeners.remove(listener); } void notifyStateUpdate(final Object input, final int type, final IViewerUpdate update) { if (!fStateUpdateListeners.isEmpty()) { for (IStateUpdateListener iStateUpdateListener : fStateUpdateListeners) { final IStateUpdateListener listener = iStateUpdateListener; SafeRunner.run(new ISafeRunnable() { @Override public void run() throws Exception { switch (type) { case STATE_SAVE_SEQUENCE_BEGINS: listener.stateSaveUpdatesBegin(input); break; case STATE_SAVE_SEQUENCE_COMPLETE: listener.stateSaveUpdatesComplete(input); break; case STATE_RESTORE_SEQUENCE_BEGINS: listener.stateRestoreUpdatesBegin(input); break; case STATE_RESTORE_SEQUENCE_COMPLETE: listener.stateRestoreUpdatesComplete(input); break; case TreeModelContentProvider.UPDATE_BEGINS: listener.stateUpdateStarted(input, update); break; case TreeModelContentProvider.UPDATE_COMPLETE: listener.stateUpdateComplete(input, update); break; default: break; } } @Override public void handleException(Throwable exception) { DebugUIPlugin.log(exception); } }); } } } }