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