| /******************************************************************************* |
| * Copyright (c) 2011, 2018 Wind River Systems and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * 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. |
| * <p> |
| * 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.<br> |
| * 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. |
| * </p><p> |
| * 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. |
| * </p> |
| * @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<K, V> extends LinkedHashMap<K, V> { |
| private static final long serialVersionUID = 1L; |
| |
| private int fMaxSize; |
| |
| LRUMap(int maxSize) { |
| super(); |
| fMaxSize = maxSize; |
| } |
| |
| @Override |
| protected boolean removeEldestEntry(Entry<K, V> 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<String, ModelDelta> 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<IStateUpdateListener> 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<IElementMementoCollector> 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<CompareRequestKey, ElementCompareRequest> 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(() -> { |
| 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((d, 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((pendingDeltaNode, 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((pendingDeltaNode, 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<ElementCompareRequest> 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 = (pendingDeltaNode, 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 = (delta, 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<IModelDelta> 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<IModelDelta> 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<IElementMementoRequest> 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<IElementMementoProvider, List<IElementMementoRequest>> 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<IElementMementoRequest> reqs = providers.get(provider); |
| if (reqs == null) { |
| reqs = new ArrayList<>(); |
| providers.put(provider, reqs); |
| } |
| reqs.add(request); |
| } |
| for (Entry<IElementMementoProvider, List<IElementMementoRequest>> entry : providers.entrySet()) { |
| IElementMementoProvider provider = entry.getKey(); |
| List<IElementMementoRequest> 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 = (delta, 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"), //$NON-NLS-1$ |
| (ModelDelta) delta)); |
| } |
| } |
| 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((delta, 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((delta, depth) -> { |
| if (depth < path.getSegmentCount()) { |
| // Descend until we reach a matching depth. |
| TreePath deltaPath1 = fContentProvider.getViewerTreePath(delta); |
| if (path.startsWith(deltaPath1, null)) { |
| return true; |
| } else { |
| return false; |
| } |
| } else if (depth == path.getSegmentCount()) { |
| TreePath deltaPath2 = fContentProvider.getViewerTreePath(delta); |
| if (deltaPath2.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() + "(" //$NON-NLS-1$ //$NON-NLS-2$ |
| + Integer.toHexString(deltaFlags & mask) + ")"); //$NON-NLS-1$ |
| } |
| } |
| ((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() + "(" //$NON-NLS-1$ //$NON-NLS-2$ |
| + Integer.toHexString(delta.getFlags()) + ")"); //$NON-NLS-1$ |
| } |
| } |
| ((ModelDelta) delta).setFlags(IModelDelta.NO_CHANGE); |
| return true; |
| } |
| }); |
| } |
| } |
| |
| |
| /** |
| * Perform any restoration required for the given tree path. |
| * <p> |
| * 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 = (delta, 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((_visitorDelta, 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. |
| * <br> |
| * 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. <code>null</code> if no reveal flag was found. |
| */ |
| private ModelDelta markRevealDelta(ModelDelta rootDelta) { |
| final ModelDelta[] revealDelta = new ModelDelta[1]; |
| IModelDeltaVisitor visitor = (delta, 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<CompareRequestKey> 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); |
| } |
| }); |
| } |
| } |
| } |
| } |