/******************************************************************************* * Copyright (c) 2006, 2018 IBM Corporation 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: * IBM Corporation - initial API and implementation * Wind River Systems - Fix for viewer state save/restore [188704] * Pawel Piech (Wind River) - added support for a virtual tree model viewer (Bug 242489) * Dorin Ciuca - Top index fix (Bug 324100) * Anton Leherbauer (Wind River) - REVEAL delta does not always work reliably (Bug 438724) *******************************************************************************/ package org.eclipse.debug.internal.ui.viewers.model; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.debug.internal.ui.DebugUIPlugin; import org.eclipse.debug.internal.ui.model.elements.ElementContentProvider; import org.eclipse.debug.internal.ui.viewers.model.provisional.ICheckboxModelProxy; import org.eclipse.debug.internal.ui.viewers.model.provisional.IChildrenUpdate; import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementContentProvider; import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelChangedListener; import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDelta; import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxy; import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxy2; import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxyFactory; import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxyFactory2; import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext; 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.TreeModelViewerFilter; import org.eclipse.jface.viewers.IContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.progress.WorkbenchJob; /** * Content provider for a virtual tree. * * @since 3.3 */ public class TreeModelContentProvider implements ITreeModelContentProvider, IContentProvider, IModelChangedListener { /** * Tree model viewer that this content provider is used with. */ private IInternalTreeModelViewer fViewer; /** * Mask used to filter delta updates coming from the model. */ private int fModelDeltaMask = ~0; /** * Map tree paths to model proxy responsible for element * * Used to install different model proxy instances for one element depending * on the tree path. */ private Map fTreeModelProxies = new HashMap<>(); // tree // model // proxy // by // element tree path /** * Map element to model proxy responsible for it. * * Used to install a single model proxy which is responsible for all * instances of an element in the model tree. */ private Map fModelProxies = new HashMap<>(); // model // proxy // by // element /** * Map of nodes that have been filtered from the viewer. */ private FilterTransform fTransform = new FilterTransform(); /** * Model listeners */ private ListenerList fModelListeners = new ListenerList<>(); /** * Viewer update listeners */ private ListenerList fUpdateListeners = new ListenerList<>(); /** * Flag indicating whether we are currently in a model sequence. * @see IViewerUpdateListener */ private boolean fModelSequenceRunning = false; /** * Map of updates in progress: element path -> list of requests */ private Map> fRequestsInProgress = new HashMap<>(); /** * Map of dependent requests waiting for parent requests to complete: * element path -> list of requests */ private Map> fWaitingRequests = new HashMap<>(); private List fCompletedUpdates = new ArrayList<>(); private Runnable fCompletedUpdatesRunnable; private ViewerStateTracker fStateTracker = new ViewerStateTracker(this); private TreePath fRevealPath; private int fRevealIndex; /** * Update type constants */ static final int UPDATE_SEQUENCE_BEGINS = 0; static final int UPDATE_SEQUENCE_COMPLETE = 1; static final int UPDATE_BEGINS = 2; static final int UPDATE_COMPLETE = 3; /** * Constant for an empty tree path. */ static final TreePath EMPTY_TREE_PATH = new TreePath(new Object[] {}); @Override public void dispose() { if (fViewer == null) { return; } fDelayedDoModelChangeJob.shutdown(); Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); // cancel pending updates for (List requests : fRequestsInProgress.values()) { for (ViewerUpdateMonitor vu : requests) { vu.cancel(); } } fWaitingRequests.clear(); fStateTracker.dispose(); fModelListeners.clear(); fUpdateListeners.clear(); disposeAllModelProxies(); synchronized(this) { fViewer = null; } } /** * @return Returns whether the content provider is disposed. */ boolean isDisposed() { synchronized(this) { return fViewer == null; } } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { synchronized(this) { fViewer = (IInternalTreeModelViewer) viewer; } Assert.isTrue( fViewer.getDisplay().getThread() == Thread.currentThread() ); if (oldInput != null) { fStateTracker.saveViewerState(oldInput); } } @Override public void postInputChanged(IInternalTreeModelViewer viewer, Object oldInput, Object newInput) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); cancelSubtreeUpdates(TreePath.EMPTY); disposeAllModelProxies(); cancelSubtreeUpdates(TreePath.EMPTY); fTransform.clear(); if (newInput != null) { installModelProxy(newInput, TreePath.EMPTY); fStateTracker.restoreViewerState(newInput); } } @Override public void addViewerUpdateListener(IViewerUpdateListener listener) { fUpdateListeners.add(listener); } @Override public void removeViewerUpdateListener(IViewerUpdateListener listener) { fUpdateListeners.remove(listener); } @Override public void addStateUpdateListener(IStateUpdateListener listener) { fStateTracker.addStateUpdateListener(listener); } @Override public void preserveState(TreePath path) { fStateTracker.appendToPendingStateDelta(path); } @Override public void removeStateUpdateListener(IStateUpdateListener listener) { fStateTracker.removeStateUpdateListener(listener); } @Override public void addModelChangedListener(IModelChangedListener listener) { fModelListeners.add(listener); } @Override public void removeModelChangedListener(IModelChangedListener listener) { fModelListeners.remove(listener); } @Override public void cancelRestore(final TreePath path, final int flags) { fStateTracker.cancelRestore(path, flags); } @Override public boolean setChecked(TreePath path, boolean checked) { IModelProxy elementProxy = getElementProxy(path); if (elementProxy instanceof ICheckboxModelProxy) { return ((ICheckboxModelProxy) elementProxy).setChecked(getPresentationContext(), getViewer().getInput(), path, checked); } return false; } /** * Installs the model proxy for the given element into this content provider * if not already installed. * @param input the input to install the model proxy on * @param path the {@link TreePath} to install the proxy for */ private void installModelProxy(Object input, TreePath path) { if (!fTreeModelProxies.containsKey(path) && !fModelProxies.containsKey(path.getLastSegment())) { Object element = path.getSegmentCount() != 0 ? path.getLastSegment() : input; IModelProxy proxy = null; IModelProxyFactory2 modelProxyFactory2 = ViewerAdapterService.getModelProxyFactory2(element); if (modelProxyFactory2 != null) { proxy = modelProxyFactory2.createTreeModelProxy(input, path, getPresentationContext()); if (proxy != null) { fTreeModelProxies.put(path, proxy); } } if (proxy == null) { IModelProxyFactory modelProxyFactory = ViewerAdapterService.getModelProxyFactory(element); if (modelProxyFactory != null) { proxy = modelProxyFactory.createModelProxy(element, getPresentationContext()); if (proxy != null) { fModelProxies.put(element, proxy); } } } if (proxy instanceof IModelProxy2) { proxy.addModelChangedListener(this); ((IModelProxy2)proxy).initialize(getViewer()); } else if (proxy != null) { final IModelProxy finalProxy = proxy; Job job = new Job("Model Proxy installed notification job") {//$NON-NLS-1$ @Override protected IStatus run(IProgressMonitor monitor) { if (!monitor.isCanceled()) { IPresentationContext context = null; Viewer viewer = null; synchronized (TreeModelContentProvider.this) { if (!isDisposed()) { context = getPresentationContext(); viewer = (Viewer) getViewer(); } } if (viewer != null && context != null && !finalProxy.isDisposed()) { finalProxy.init(context); finalProxy.addModelChangedListener(TreeModelContentProvider.this); finalProxy.installed(viewer); } } return Status.OK_STATUS; } @Override public boolean shouldRun() { return !isDisposed(); } }; job.setSystem(true); job.schedule(); } } } /** * Finds the model proxy that an element with a given path is associated with. * @param path Path of the element. * @return Element's model proxy. */ private IModelProxy getElementProxy(TreePath path) { while (path != null) { IModelProxy proxy = fTreeModelProxies.get(path); if (proxy != null) { return proxy; } Object element = path.getSegmentCount() == 0 ? getViewer().getInput() : path.getLastSegment(); proxy = fModelProxies.get(element); if (proxy != null) { return proxy; } path = path.getParentPath(); } return null; } /** * Removes all model proxies */ private void disposeAllModelProxies() { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); for (IModelProxy proxy : fModelProxies.values()) { proxy.dispose(); } fModelProxies.clear(); for (IModelProxy proxy : fTreeModelProxies.values()) { proxy.dispose(); } fTreeModelProxies.clear(); } /** * Removes the model proxy installed for the given element, if any. * @param path the {@link TreePath} to dispose the model proxy for */ private void disposeModelProxy(TreePath path) { IModelProxy proxy = fTreeModelProxies.remove(path); if (proxy != null) { proxy.dispose(); } proxy = fModelProxies.remove(path.getLastSegment()); if (proxy != null) { proxy.dispose(); } } private static class DelayedDoModelChange { public final IModelDelta delta; public final IModelProxy proxy; public DelayedDoModelChange(final IModelDelta delta, final IModelProxy proxy) { this.delta = delta; this.proxy = proxy; } } private class DelayedDoModelChangedJob extends WorkbenchJob { // queue of submitted deltas to process private final List fQueue = new ArrayList<>(); private boolean shutdown; public DelayedDoModelChangedJob() { super("Delayed model change job"); //$NON-NLS-1$ setSystem(true); setUser(false); } @Override public IStatus runInUIThread(IProgressMonitor monitor) { List currentBatch = new ArrayList<>(); synchronized (fQueue) { if (shutdown || fQueue.isEmpty()) { return Status.OK_STATUS; } currentBatch = new ArrayList<>(fQueue); fQueue.clear(); } if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER) { DebugUIPlugin.trace("Delayed batch size: " + currentBatch.size()); //$NON-NLS-1$ } for (Iterator iterator = currentBatch.iterator(); iterator.hasNext();) { Object task = iterator.next(); if (monitor.isCanceled()) { restoreQueue(currentBatch); return Status.CANCEL_STATUS; } if (task instanceof DelayedDoModelChange) { DelayedDoModelChange change = (DelayedDoModelChange) task; if (!change.proxy.isDisposed()) { doModelChanged(change.delta, change.proxy); } } else { ((Runnable) task).run(); } iterator.remove(); } return Status.OK_STATUS; } private void restoreQueue(List currentBatch) { synchronized (fQueue) { currentBatch.addAll(fQueue); fQueue.clear(); fQueue.addAll(currentBatch); } } public void runDelayed(final IModelDelta delta, final IModelProxy proxy) { runDelayed(new DelayedDoModelChange(delta, proxy)); } public void runDelayed(final Runnable uiTask) { runDelayed((Object) uiTask); } private void runDelayed(final Object task) { synchronized (fQueue) { if (shutdown) { return; } fQueue.add(task); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER) { DebugUIPlugin.trace("Delayed queue size: " + fQueue.size()); //$NON-NLS-1$ } } schedule(); } public void shutdown() { synchronized (fQueue) { shutdown = true; fQueue.clear(); } cancel(); } @Override public boolean belongsTo(Object family) { return family == TreeModelContentProvider.this || family == TreeModelContentProvider.class; } } private final DelayedDoModelChangedJob fDelayedDoModelChangeJob = new DelayedDoModelChangedJob(); @Override public void modelChanged(final IModelDelta delta, final IModelProxy proxy) { Display display = null; // Check if the viewer is still available, i.e. if the content provider // is not disposed. synchronized(this) { if (fViewer != null && !proxy.isDisposed()) { display = fViewer.getDisplay(); } } if (display != null) { // If we're in display thread, process the delta immediately to // avoid "skid" in processing events. if (Thread.currentThread().equals(display.getThread())) { doModelChanged(delta, proxy); } else { try { Job.getJobManager().join(ElementContentProvider.class, null); } catch (OperationCanceledException | InterruptedException e) { DebugUIPlugin.log(new Status(IStatus.WARNING, DebugUIPlugin.getUniqueIdentifier(), "Interrupted while waiting on ElementContentProvider jobs", e)); //$NON-NLS-1$ } fDelayedDoModelChangeJob.runDelayed(delta, proxy); } } } /** * Executes the model proxy in UI thread. * @param delta Delta to process * @param proxy Proxy that fired the delta. */ private void doModelChanged(IModelDelta delta, IModelProxy proxy) { if (!proxy.isDisposed()) { if (DebugUIPlugin.DEBUG_DELTAS && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("RECEIVED DELTA: " + delta.toString()); //$NON-NLS-1$ } updateModel(delta, getModelDeltaMask()); // Initiate model update sequence before notifying of the model changed. trigger(null); // Call model listeners after updating the viewer model. for (IModelChangedListener iModelChangedListener : fModelListeners) { iModelChangedListener.modelChanged(delta, proxy); } } } @Override public void setModelDeltaMask(int mask) { fModelDeltaMask = mask; } @Override public int getModelDeltaMask() { return fModelDeltaMask; } @Override public void updateModel(IModelDelta delta, int mask) { // Processing deltas with null input leads to NPEs // (Bug 380288 - NPE switching to the Breakpoints View) if (getViewer() == null || getViewer().getInput() == null) { return; } fRevealPath = null; IModelDelta[] deltaArray = new IModelDelta[] { delta }; updateNodes(deltaArray, mask & (IModelDelta.REMOVED | IModelDelta.UNINSTALL)); updateNodes(deltaArray, mask & ITreeModelContentProvider.UPDATE_MODEL_DELTA_FLAGS & ~(IModelDelta.REMOVED | IModelDelta.UNINSTALL)); updateNodes(deltaArray, mask & ITreeModelContentProvider.CONTROL_MODEL_DELTA_FLAGS); fStateTracker.checkIfRestoreComplete(); } /** * Returns a tree path for the node including the root element. * * @param node * model delta * @return corresponding tree path */ TreePath getFullTreePath(IModelDelta node) { ArrayList list = new ArrayList<>(); while (node.getParentDelta() != null) { list.add(0, node.getElement()); node = node.getParentDelta(); } return new TreePath(list.toArray()); } /** * Returns a tree path for the node, *not* including the root element. * * @param node * model delta * @return corresponding tree path */ TreePath getViewerTreePath(IModelDelta node) { ArrayList list = new ArrayList<>(); IModelDelta parentDelta = node.getParentDelta(); while (parentDelta != null) { list.add(0, node.getElement()); node = parentDelta; parentDelta = node.getParentDelta(); } return new TreePath(list.toArray()); } /** * Returns the viewer this content provider is working for. * * @return viewer */ protected IInternalTreeModelViewer getViewer() { synchronized(this) { return fViewer; } } @Override public int viewToModelIndex(TreePath parentPath, int index) { return fTransform.viewToModelIndex(parentPath, index); } @Override public int viewToModelCount(TreePath parentPath, int count) { return fTransform.viewToModelCount(parentPath, count); } @Override public int modelToViewIndex(TreePath parentPath, int index) { return fTransform.modelToViewIndex(parentPath, index); } @Override public int modelToViewChildCount(TreePath parentPath, int count) { return fTransform.modelToViewCount(parentPath, count); } @Override public boolean areTreeModelViewerFiltersApplicable(Object parentElement) { ViewerFilter[] filters = fViewer.getFilters(); if (filters.length > 0) { for (int j = 0; j < filters.length; j++) { if (filters[j] instanceof TreeModelViewerFilter && ((TreeModelViewerFilter)filters[j]).isApplicable(fViewer, parentElement)) { return true; } } } return false; } @Override public boolean shouldFilter(Object parentElementOrTreePath, Object element) { ViewerFilter[] filters = fViewer.getFilters(); if (filters.length > 0) { for (int j = 0; j < filters.length; j++) { if (filters[j] instanceof TreeModelViewerFilter) { // Skip the filter if not applicable to parent element Object parentElement = parentElementOrTreePath instanceof TreePath ? ((TreePath)parentElementOrTreePath).getLastSegment() : parentElementOrTreePath; if (parentElement == null) { parentElement = fViewer.getInput(); } if (!((TreeModelViewerFilter)filters[j]).isApplicable(fViewer, parentElement)) { continue; } } if (!(filters[j].select((Viewer) fViewer, parentElementOrTreePath, element))) { return true; } } } return false; } @Override public void unmapPath(TreePath path) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); fTransform.clear(path); cancelSubtreeUpdates(path); } boolean addFilteredIndex(TreePath parentPath, int index, Object element) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); return fTransform.addFilteredIndex(parentPath, index, element); } void removeElementFromFilters(TreePath parentPath, int index) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); fTransform.removeElementFromFilters(parentPath, index); } boolean removeElementFromFilters(TreePath parentPath, Object element) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); return fTransform.removeElementFromFilters(parentPath, element); } void setModelChildCount(TreePath parentPath, int childCount) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); fTransform.setModelChildCount(parentPath, childCount); } boolean isFiltered(TreePath parentPath, int index) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); return fTransform.isFiltered(parentPath, index); } int[] getFilteredChildren(TreePath parent) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); return fTransform.getFilteredChildren(parent); } void clearFilteredChild(TreePath parent, int modelIndex) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); fTransform.clear(parent, modelIndex); } void clearFilters(TreePath parent) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); fTransform.clear(parent); } /** * Notification an update request has started * * @param update the update to notify about */ void updateStarted(ViewerUpdateMonitor update) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); List requests = fRequestsInProgress.get(update.getSchedulingPath()); if (requests == null) { requests = new ArrayList<>(); fRequestsInProgress.put(update.getSchedulingPath(), requests); } requests.add(update); if (!fModelSequenceRunning) { fModelSequenceRunning = true; if (DebugUIPlugin.DEBUG_UPDATE_SEQUENCE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("MODEL SEQUENCE BEGINS"); //$NON-NLS-1$ } notifyUpdate(UPDATE_SEQUENCE_BEGINS, null); } if (DebugUIPlugin.DEBUG_UPDATE_SEQUENCE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("\tBEGIN - " + update); //$NON-NLS-1$ } notifyUpdate(UPDATE_BEGINS, update); } /** * Notification an update request has completed * * @param updates the updates to notify */ void updatesComplete(final List updates) { for (int i = 0; i < updates.size(); i++) { ViewerUpdateMonitor update = updates.get(i); notifyUpdate(UPDATE_COMPLETE, update); if (DebugUIPlugin.DEBUG_UPDATE_SEQUENCE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("\tEND - " + update); //$NON-NLS-1$ } } // Wait a single cycle to allow viewer to queue requests triggered by completed updates. getViewer().getDisplay().asyncExec(() -> { if (isDisposed()) { return; } for (int i = 0; i < updates.size(); i++) { ViewerUpdateMonitor update = updates.get(i); // Search for update in list using identity test. Otherwise a completed canceled // update may trigger removal of up-to-date running update on the same element. List requests = fRequestsInProgress.get(update.getSchedulingPath()); boolean found = false; if (requests != null) { for (int j = 0; j < requests.size(); j++) { if (requests.get(j) == update) { found = true; requests.remove(j); break; } } } if (found) { // Trigger may initiate new updates, so wait to remove requests array from // fRequestsInProgress map. This way updateStarted() will not send a // redundant "UPDATE SEQUENCE STARTED" notification. trigger(update.getSchedulingPath()); } else { // Update may be removed from in progress list if it was canceled by schedule(). Assert.isTrue(update.isCanceled()); } if (requests != null && requests.isEmpty()) { fRequestsInProgress.remove(update.getSchedulingPath()); } } if (fRequestsInProgress.isEmpty() && fWaitingRequests.isEmpty() && fModelSequenceRunning) { fModelSequenceRunning = false; if (fRevealPath != null) { getViewer().reveal(fRevealPath, fRevealIndex); fRevealPath = null; } if (DebugUIPlugin.DEBUG_UPDATE_SEQUENCE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("MODEL SEQUENCE ENDS"); //$NON-NLS-1$ } notifyUpdate(UPDATE_SEQUENCE_COMPLETE, null); } }); } /** * @return Returns true if there are outstanding updates in the viewer. */ boolean areRequestsPending() { return !fRequestsInProgress.isEmpty() || !fWaitingRequests.isEmpty(); } /** * @return Returns the state tracker for the content provider. */ ViewerStateTracker getStateTracker() { return fStateTracker; } /** * Notifies listeners about given update. * @param type Type of update to call listeners with. * @param update Update to notify about. */ private void notifyUpdate(final int type, final IViewerUpdate update) { if (!fUpdateListeners.isEmpty()) { for (IViewerUpdateListener iViewerUpdateListener : fUpdateListeners) { final IViewerUpdateListener listener = iViewerUpdateListener; SafeRunner.run(new ISafeRunnable() { @Override public void run() throws Exception { switch (type) { case UPDATE_SEQUENCE_BEGINS: listener.viewerUpdatesBegin(); break; case UPDATE_SEQUENCE_COMPLETE: listener.viewerUpdatesComplete(); break; case UPDATE_BEGINS: listener.updateStarted(update); break; case UPDATE_COMPLETE: listener.updateComplete(update); break; default: break; } } @Override public void handleException(Throwable exception) { DebugUIPlugin.log(exception); } }); } } } /** * Cancels outstanding updates for the element at given path and its * children. * @param path Path of element. */ private void cancelSubtreeUpdates(TreePath path) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); for (Entry> entry : fRequestsInProgress.entrySet()) { TreePath entryPath = entry.getKey(); if (entryPath.startsWith(path, null)) { List requests = entry.getValue(); Iterator reqIter = requests.iterator(); while (reqIter.hasNext()) { // Cancel update and remove from requests list. Removing from // fRequestsInProgress ensures that isRequestBlocked() won't be triggered // by a canceled update. reqIter.next().cancel(); reqIter.remove(); } } } List purge = new ArrayList<>(); for (TreePath entryPath : fWaitingRequests.keySet()) { if (entryPath.startsWith(path, null)) { purge.add(entryPath); } } for (TreePath tp : purge) { fWaitingRequests.remove(tp); } fStateTracker.cancelStateSubtreeUpdates(path); } /** * Returns whether this given request should be run, or should wait for * parent update to complete. * * @param update the update the schedule */ private void schedule(final ViewerUpdateMonitor update) { Assert.isTrue(getViewer().getDisplay().getThread() == Thread.currentThread()); TreePath schedulingPath = update.getSchedulingPath(); List requests = fWaitingRequests.get(schedulingPath); if (requests == null) { requests = new LinkedList<>(); requests.add(update); fWaitingRequests.put(schedulingPath, requests); List inProgressList = fRequestsInProgress.get(schedulingPath); if (inProgressList != null) { int staleUpdateIndex = inProgressList.indexOf(update); if (staleUpdateIndex >= 0) { // Cancel update and remove from requests list. Removing from // fRequestsInProgress ensures that isRequestBlocked() won't be triggered // by a canceled update. ViewerUpdateMonitor staleUpdate = inProgressList.remove(staleUpdateIndex); staleUpdate.cancel(); // Note: Do not reset the inProgressList to null. This would cause the // updateStarted() method to think that a new update sequence is // being started. Since there are waiting requests for this scheduling // path, the list will be cleaned up later. } } if (inProgressList == null || inProgressList.isEmpty()) { getViewer().getDisplay().asyncExec(() -> { if (isDisposed()) { return; } trigger(update.getSchedulingPath()); }); } } else { // there are waiting requests: coalesce with existing request and add to list requests.add(coalesce(requests, update)); } } /** * Tries to coalesce the given request with any request in the list. If a match is found, * the resulting request is then coalesced again with candidates in list. * @param requests List of waiting requests to coalesce with * @param toCoalesce request to coalesce * @return Returns either the coalesced request. If no match was found it returns the * toCoalesce parameter request. Either way the returned request needs to be added to the * waiting requests list. */ private ViewerUpdateMonitor coalesce(List requests, ViewerUpdateMonitor toCoalesce) { for (ViewerUpdateMonitor waiting : requests) { if (waiting.coalesce(toCoalesce)) { requests.remove(waiting); // coalesced with existing request, done // try to coalesce the combined requests with other waiting requests return coalesce(requests, waiting); } } return toCoalesce; } /** * Returns whether there are outstanding ChildrenUpdate updates for the given path. * This method is expected to be called during processing of a ChildrenRequest, * therefore one running children request is ignored. * @param path Path of element to check. * @return True if there are outstanding children updates for given element. */ boolean areChildrenUpdatesPending(TreePath path) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); List requests = fWaitingRequests.get(path); if (requests != null) { for (int i = 0; i < requests.size(); i++) { if (requests.get(i) instanceof ChildrenUpdate) { return true; } } } requests = fRequestsInProgress.get(path); if (requests != null) { int numChildrenUpdateRequests = 0; for (int i = 0; i < requests.size(); i++) { if (requests.get(i) instanceof ChildrenUpdate) { if (++numChildrenUpdateRequests > 1) { return true; } } } } return false; } /** * Triggers waiting requests based on the given request that just * completed. *

* Requests are processed in order such that updates to * children are delayed until updates for parent elements are completed. * This allows the expansion/selection state of the elements to be * properly restored as new elements are retrieved from model. *

* @param schedulingPath schedulingPath path or requests to start processing. May * be null to start the shortest path request. */ private void trigger(TreePath schedulingPath) { Assert.isTrue(getViewer().getDisplay().getThread() == Thread.currentThread()); if (fWaitingRequests.isEmpty()) { return; } List waiting = fWaitingRequests.get(schedulingPath); if (waiting == null) { // no waiting, update the entry with the shortest path int length = Integer.MAX_VALUE; Entry> candidate = null; for (Entry> entry : fWaitingRequests.entrySet()) { TreePath key = entry.getKey(); if (key.getSegmentCount() < length && !isRequestBlocked(key)) { candidate = entry; length = key.getSegmentCount(); } } if (candidate != null) { startHighestPriorityRequest(candidate.getKey(), candidate.getValue()); } } else if (!isRequestBlocked(schedulingPath)) { // start the highest priority request startHighestPriorityRequest(schedulingPath, waiting); } } /** * Returns true if there are running requests for any parent element of * the given tree path. * @param requestPath Path of element to check. * @return Returns true if requests are running. */ private boolean isRequestBlocked(TreePath requestPath) { TreePath parentPath = requestPath; List parentRequests = fRequestsInProgress.get(parentPath); while (parentRequests == null || parentRequests.isEmpty()) { parentPath = parentPath.getParentPath(); if (parentPath == null) { // no running requests: start request return false; } parentRequests = fRequestsInProgress.get(parentPath); } return true; } /** * @param key the {@link TreePath} * @param waiting the list of waiting requests */ private void startHighestPriorityRequest(TreePath key, List waiting) { int priority = 4; ViewerUpdateMonitor next = null; for (ViewerUpdateMonitor vu : waiting) { if (vu.getPriority() < priority) { next = vu; priority = next.getPriority(); } } if (next != null) { waiting.remove(next); if (waiting.isEmpty()) { fWaitingRequests.remove(key); } next.start(); } } /** * Returns the element corresponding to the given tree path. * * @param path * tree path * @return model element */ protected Object getElement(TreePath path) { if (path.getSegmentCount() > 0) { return path.getLastSegment(); } return getViewer().getInput(); } /** * Reschedule any children updates in progress for the given parent that * have a start index greater than the given index. An element has been * removed at this index, invalidating updates in progress. * * @param parentPath * view tree path to parent element * @param modelIndex * index at which an element was removed */ private void rescheduleUpdates(TreePath parentPath, int modelIndex) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); List requests = fRequestsInProgress.get(parentPath); List reCreate = null; if (requests != null) { Iterator iterator = requests.iterator(); while (iterator.hasNext()) { ViewerUpdateMonitor update = iterator.next(); if (update instanceof IChildrenUpdate) { IChildrenUpdate childrenUpdate = (IChildrenUpdate) update; if (childrenUpdate.getOffset() > modelIndex) { // Cancel update and remove from requests list. Removing from // fRequestsInProgress ensures that isRequestBlocked() won't be triggered // by a canceled update. childrenUpdate.cancel(); iterator.remove(); if (reCreate == null) { reCreate = new ArrayList<>(); } reCreate.add(childrenUpdate); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("canceled update in progress handling REMOVE: " + childrenUpdate); //$NON-NLS-1$ } } } } } requests = fWaitingRequests.get(parentPath); if (requests != null) { for (ViewerUpdateMonitor update : requests) { if (update instanceof IChildrenUpdate) { IChildrenUpdate childrenUpdate = (IChildrenUpdate) update; if (childrenUpdate.getOffset() > modelIndex) { ((ChildrenUpdate) childrenUpdate).setOffset(childrenUpdate.getOffset() - 1); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("modified waiting update handling REMOVE: " + childrenUpdate); //$NON-NLS-1$ } } } } } // re-schedule canceled updates at new position. // have to do this last else the requests would be waiting and // get modified. if (reCreate != null) { for (IChildrenUpdate childrenUpdate : reCreate) { int start = childrenUpdate.getOffset() - 1; int end = start + childrenUpdate.getLength(); for (int i = start; i < end; i++) { doUpdateElement(parentPath, i); } } } } private void doUpdateChildCount(TreePath path) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); Object element = getElement(path); IElementContentProvider contentAdapter = ViewerAdapterService.getContentProvider(element); if (contentAdapter != null) { ChildrenCountUpdate request = new ChildrenCountUpdate(this, getViewer().getInput(), path, element, contentAdapter); schedule(request); } } void doUpdateElement(TreePath parentPath, int modelIndex) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); Object parent = getElement(parentPath); IElementContentProvider contentAdapter = ViewerAdapterService.getContentProvider(parent); if (contentAdapter != null) { ChildrenUpdate request = new ChildrenUpdate(this, getViewer().getInput(), parentPath, parent, modelIndex, contentAdapter); schedule(request); } } private void doUpdateHasChildren(TreePath path) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); Object element = getElement(path); IElementContentProvider contentAdapter = ViewerAdapterService.getContentProvider(element); if (contentAdapter != null) { HasChildrenUpdate request = new HasChildrenUpdate(this, getViewer().getInput(), path, element, contentAdapter); schedule(request); } } /** * Checks if there are outstanding updates that may replace the element * at given path. * @param path Path of element to check. * @return Returns true if there are outstanding updates. */ boolean areElementUpdatesPending(TreePath path) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); TreePath parentPath = path.getParentPath(); List requests = fWaitingRequests.get(path); if (requests != null) { for (int i = 0; i < requests.size(); i++) { ViewerUpdateMonitor update = requests.get(i); if (update instanceof ChildrenUpdate) { return true; } } } requests = fWaitingRequests.get(parentPath); if (requests != null) { for (int i = 0; i < requests.size(); i++) { ViewerUpdateMonitor update = requests.get(i); if (update.containsUpdate(path)) { return true; } } } requests = fRequestsInProgress.get(path); if (requests != null) { for (int i = 0; i < requests.size(); i++) { ViewerUpdateMonitor update = requests.get(i); if (update instanceof ChildrenUpdate) { return true; } } } requests = fRequestsInProgress.get(parentPath); if (requests != null) { for (int i = 0; i < requests.size(); i++) { ViewerUpdateMonitor update = requests.get(i); if (update.getElement().equals(path.getLastSegment())) { return true; } } } return false; } /** * Returns the presentation context for this content provider. * * @return presentation context */ protected IPresentationContext getPresentationContext() { ITreeModelViewer viewer = getViewer(); if (viewer != null) { return viewer.getPresentationContext(); } return null; } /** * Updates the viewer with the following deltas. * * @param nodes Model deltas to be processed. * @param mask the model delta mask * @see IModelDelta for a list of masks */ protected void updateNodes(IModelDelta[] nodes, int mask) { for (int i = 0; i < nodes.length; i++) { IModelDelta node = nodes[i]; int flags = node.getFlags() & mask; if (flags != 0) { if ((flags & IModelDelta.ADDED) != 0) { handleAdd(node); } if ((flags & IModelDelta.REMOVED) != 0) { handleRemove(node); } if ((flags & IModelDelta.CONTENT) != 0) { handleContent(node); } if ((flags & IModelDelta.STATE) != 0) { handleState(node); } if ((flags & IModelDelta.INSERTED) != 0) { handleInsert(node); } if ((flags & IModelDelta.REPLACED) != 0) { handleReplace(node); } if ((flags & IModelDelta.INSTALL) != 0) { handleInstall(node); } if ((flags & IModelDelta.UNINSTALL) != 0) { handleUninstall(node); } if ((flags & IModelDelta.EXPAND) != 0) { handleExpand(node); } if ((flags & IModelDelta.COLLAPSE) != 0) { handleCollapse(node); } if ((flags & IModelDelta.SELECT) != 0) { handleSelect(node); } if ((flags & IModelDelta.REVEAL) != 0) { handleReveal(node); } } updateNodes(node.getChildDeltas(), mask); } } protected void handleInstall(IModelDelta delta) { installModelProxy(getViewer().getInput(), getFullTreePath(delta)); } protected void handleUninstall(IModelDelta delta) { disposeModelProxy(getFullTreePath(delta)); } protected void handleAdd(IModelDelta delta) { IModelDelta parentDelta = delta.getParentDelta(); if (parentDelta == null) { DebugUIPlugin.log(new Status(IStatus.ERROR, DebugUIPlugin.getUniqueIdentifier(), "Invalid viewer update: " + delta + ", in " + getPresentationContext().getId(), null )); //$NON-NLS-1$ //$NON-NLS-2$ return; } TreePath parentPath = getViewerTreePath(parentDelta); Object element = delta.getElement(); int count = parentDelta.getChildCount(); if (count > 0) { setModelChildCount(parentPath, count); int modelIndex = count - 1; if (delta.getIndex() != -1) { // assume addition at end, unless index specified by delta modelIndex = delta.getIndex(); } if (shouldFilter(parentPath, element)) { addFilteredIndex(parentPath, modelIndex, element); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[filtered] handleAdd(" + delta.getElement() + ") > modelIndex: " + modelIndex); //$NON-NLS-1$ //$NON-NLS-2$ } // it was filtered so the child count does not change } else { if (isFiltered(parentPath, modelIndex)) { clearFilteredChild(parentPath, modelIndex); } int viewIndex = modelToViewIndex(parentPath, modelIndex); int viewCount = modelToViewChildCount(parentPath, count); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("handleAdd(" + delta.getElement() + ") viewIndex: " + viewIndex + " modelIndex: " + modelIndex + " viewCount: " + viewCount + " modelCount: " + count); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ } getViewer().setChildCount(parentPath, viewCount); getViewer().autoExpand(parentPath); getViewer().replace(parentPath, viewIndex, element); TreePath childPath = parentPath.createChildPath(element); updateHasChildren(childPath); fStateTracker.restorePendingStateOnUpdate(childPath, modelIndex, false, false, false); } } else { if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("handleAdd(" + delta.getElement() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } doUpdateChildCount(getViewerTreePath(delta.getParentDelta())); } } protected void handleContent(IModelDelta delta) { if (delta.getParentDelta() == null && delta.getChildCount() == 0) { // if the delta is for the root, ensure the root still matches viewer input if (!delta.getElement().equals(getViewer().getInput())) { return; } } TreePath treePath = getViewerTreePath(delta); cancelSubtreeUpdates(treePath); getViewer().refresh(getElement(treePath)); } protected void handleCollapse(IModelDelta delta) { TreePath elementPath = getViewerTreePath(delta); getViewer().setExpandedState(elementPath, false); cancelRestore(elementPath, IModelDelta.EXPAND); } protected void handleExpand(IModelDelta delta) { // expand each parent, then this node IModelDelta parentDelta = delta.getParentDelta(); if (parentDelta != null) { if ((parentDelta.getFlags() & IModelDelta.EXPAND) == 0) { handleExpand(parentDelta); } expand(delta); } else { int childCount = delta.getChildCount(); TreePath elementPath = getViewerTreePath(delta); if (childCount > 0) { int viewCount = modelToViewChildCount(elementPath, childCount); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[expand] setChildCount(" + delta.getElement() + ", (model) " + childCount + " (view) " + viewCount); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } getViewer().setChildCount(elementPath, viewCount); } } } /** * Expands the element pointed to by given delta. * @param delta Delta that points to the element to expand. */ private void expand(IModelDelta delta) { int childCount = delta.getChildCount(); int modelIndex = delta.getIndex(); IInternalTreeModelViewer treeViewer = getViewer(); TreePath elementPath = getViewerTreePath(delta); if (modelIndex >= 0) { TreePath parentPath = elementPath.getParentPath(); if (parentPath == null) { parentPath = TreePath.EMPTY; } int viewIndex = modelToViewIndex(parentPath, modelIndex); if (viewIndex >= 0) { if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[expand] replace(" + delta.getParentDelta().getElement() + ", (model) " + modelIndex + " (view) " + viewIndex + ", " + delta.getElement()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } treeViewer.replace(parentPath, viewIndex, delta.getElement()); } else { // Element is filtered - if no longer filtered, insert the element viewIndex = unfilterElement(parentPath, delta.getElement(), modelIndex); if (viewIndex < 0) { // insert did not complete return; } } } if (childCount > 0) { int viewCount = modelToViewChildCount(elementPath, childCount); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[expand] setChildCount(" + delta.getElement() + ", (model) " + childCount + " (view) " + viewCount); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } treeViewer.setChildCount(elementPath, viewCount); if (!treeViewer.getExpandedState(elementPath)) { treeViewer.expandToLevel(elementPath, 1); cancelRestore(elementPath, IModelDelta.COLLAPSE); } } } /** * Inserts the given child element of the specified parent into the tree if the element * should *no* longer be filtered. Returns the view index of the newly inserted element * or -1 if not inserted. * * @param parentPath viewer tree path to parent element * @param element element to insert * @param modelIndex index of the element in the model * @return Returns the view index of the newly inserted element * or -1 if not inserted. */ private int unfilterElement(TreePath parentPath, Object element, int modelIndex) { // Element is filtered - if no longer filtered, insert the element if (shouldFilter(parentPath, element)) { if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[unfilter] abort unfilter element: " + element + ", (model) " + modelIndex); //$NON-NLS-1$ //$NON-NLS-2$ } // still filtered, stop return -1; } // clear the filter an insert the element clearFilteredChild(parentPath, modelIndex); int viewIndex = modelToViewIndex(parentPath, modelIndex); if (viewIndex >= 0) { if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[unfilter] insert(" + parentPath.getLastSegment() + ", (model) " + modelIndex + " (view) " + viewIndex + ", " + element); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } getViewer().insert(parentPath, element, viewIndex); return viewIndex; } else { // still filtered - should not happen return -1; } } protected void handleInsert(IModelDelta delta) { IModelDelta parentDelta = delta.getParentDelta(); if (parentDelta == null) { DebugUIPlugin.log(new Status(IStatus.ERROR, DebugUIPlugin.getUniqueIdentifier(), "Invalid viewer update: " + delta + ", in " + getPresentationContext().getId(), null )); //$NON-NLS-1$ //$NON-NLS-2$ return; } TreePath parentPath = getViewerTreePath(delta.getParentDelta()); Object element = delta.getElement(); int modelIndex = delta.getIndex(); int viewIndex = modelIndex >= 0 ? modelToViewIndex(parentPath, modelIndex) : -1; int viewCount = getViewer().getChildCount(parentPath); if (viewIndex >= 0 && viewIndex <= viewCount) { // Index in range, insert. if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("handleInsert(" + parentPath.getLastSegment() + ", (model) " + modelIndex + " (view) " + viewIndex + ", " + element); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } getViewer().insert(parentPath, element, viewIndex); } else if (modelIndex >= 0 && viewIndex < 0) { // Element is filtered - do not insert if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[filtered] handleInsert(" + delta.getElement() + ") > modelIndex: " + modelIndex); //$NON-NLS-1$ //$NON-NLS-2$ } } else { // Invalid index given, do a refresh instead. if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("handleInsert(" + delta.getElement() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } doUpdateChildCount(getViewerTreePath(delta.getParentDelta())); } } protected void handleRemove(IModelDelta delta) { if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("handleRemove(" + delta.getElement() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } IModelDelta parentDelta = delta.getParentDelta(); if (parentDelta == null) { DebugUIPlugin.log(new Status(IStatus.ERROR, DebugUIPlugin.getUniqueIdentifier(), "Invalid viewer update: " + delta + ", in " + getPresentationContext().getId(), null )); //$NON-NLS-1$ //$NON-NLS-2$ return; } IInternalTreeModelViewer treeViewer = getViewer(); TreePath parentPath = getViewerTreePath(parentDelta); Object element = delta.getElement(); if (removeElementFromFilters(parentPath, element)) { // element was filtered - done return; } int viewIndex = -1; int modelIndex = delta.getIndex(); int unmappedIndex = -1; int itemCount = -1; if (modelIndex < 0) { itemCount = treeViewer.getChildCount(parentPath); if (itemCount == -1) { clearFilters(parentPath); } viewIndex = treeViewer.findElementIndex(parentPath, element); if (viewIndex >= 0) { modelIndex = viewToModelIndex(parentPath, viewIndex); } else { unmappedIndex = treeViewer.findElementIndex(parentPath, null); } } else { viewIndex = modelToViewIndex(parentPath, modelIndex); } if (modelIndex >= 0) { // found the element if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace(" - (found) remove(" + parentPath.getLastSegment() + ", viewIndex: " + viewIndex + " modelIndex: " + modelIndex); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } rescheduleUpdates(parentPath, modelIndex); getViewer().remove(parentPath, viewIndex); removeElementFromFilters(parentPath, modelIndex); return; } if (unmappedIndex >= 0) { // did not find the element, but found an unmapped item. // remove the unmapped item in it's place and update filters if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace(" - (not found) remove(" + parentPath.getLastSegment() + ", viewIndex: " + viewIndex + " modelIndex: " + modelIndex + " unmapped index: " + unmappedIndex); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } modelIndex = viewToModelIndex(parentPath, unmappedIndex); rescheduleUpdates(parentPath, modelIndex); getViewer().remove(parentPath, unmappedIndex); removeElementFromFilters(parentPath, modelIndex); return; } int modelCount = parentDelta.getChildCount(); if (itemCount >= 0 && modelCount >= 0) { if (modelToViewChildCount(parentPath, modelCount) == itemCount) { // item count matches the parent's child count, don't do anything return; } } // failing that, refresh the parent to properly update for non-visible/unmapped children // and update filtered indexes if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace(" - (not found) remove/refresh(" + delta.getElement()); //$NON-NLS-1$ } getViewer().remove(getViewerTreePath(delta)); clearFilters(parentPath); getViewer().refresh(parentDelta.getElement()); } protected void handleReplace(IModelDelta delta) { IModelDelta parentDelta = delta.getParentDelta(); if (parentDelta == null) { DebugUIPlugin.log(new Status(IStatus.ERROR, DebugUIPlugin.getUniqueIdentifier(), "Invalid viewer update: " + delta + ", in " + getPresentationContext().getId(), null )); //$NON-NLS-1$ //$NON-NLS-2$ return; } TreePath parentPath = getViewerTreePath(parentDelta); int index = delta.getIndex(); if (index < 0) { index = fTransform.indexOfFilteredElement(parentPath, delta.getElement()); } if (index >= 0) { boolean filtered = isFiltered(parentPath, index); boolean shouldFilter = shouldFilter(parentPath, delta.getReplacementElement()); // Update the filter transform if (filtered) { clearFilteredChild(parentPath, index); } if (shouldFilter) { addFilteredIndex(parentPath, index, delta.getElement()); } // Update the viewer if (filtered) { if (!shouldFilter) { getViewer().insert(parentPath, delta.getReplacementElement(), modelToViewIndex(parentPath, index)); } //else do nothing } else { if (shouldFilter) { getViewer().remove(parentPath, modelToViewIndex(parentPath, index)); } else { getViewer().replace(parentPath, delta.getIndex(), delta.getReplacementElement()); } } } } protected void handleSelect(IModelDelta delta) { int modelIndex = delta.getIndex(); IInternalTreeModelViewer treeViewer = getViewer(); // check if selection is allowed IStructuredSelection candidate = new TreeSelection(getViewerTreePath(delta)); if ((delta.getFlags() & IModelDelta.FORCE) == 0 && !treeViewer.overrideSelection(treeViewer.getSelection(), candidate)) { return; } // empty the selection before replacing elements to avoid materializing elements (@see bug 305739) treeViewer.clearSelectionQuiet(); if (modelIndex >= 0) { IModelDelta parentDelta = delta.getParentDelta(); if (parentDelta == null) { DebugUIPlugin.log(new Status(IStatus.ERROR, DebugUIPlugin.getUniqueIdentifier(), "Invalid viewer update: " + delta + ", in " + getPresentationContext().getId(), null )); //$NON-NLS-1$ //$NON-NLS-2$ return; } TreePath parentPath = getViewerTreePath(parentDelta); int viewIndex = modelToViewIndex(parentPath, modelIndex); if (viewIndex >= 0) { // when viewIndex < 0, the element has been filtered - so we should not try to replace int modelCount = parentDelta.getChildCount(); if (modelCount > 0) { int viewCount = modelToViewChildCount(parentPath, modelCount); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[select] setChildCount(" + parentDelta.getElement() + ", (model) " + parentDelta.getChildCount() + " (view) " + viewCount ); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } treeViewer.setChildCount(parentPath, viewCount); } if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[select] replace(" + parentDelta.getElement() + ", (model) " + modelIndex + " (view) " + viewIndex + ", " + delta.getElement()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } treeViewer.replace(parentPath, viewIndex, delta.getElement()); } } TreePath selectionPath = getViewerTreePath(delta); if (treeViewer.trySelection(new TreeSelection(selectionPath), false, (delta.getFlags() | IModelDelta.FORCE) == 0)) { cancelRestore(selectionPath, IModelDelta.SELECT); } } protected void handleState(IModelDelta delta) { getViewer().update(delta.getElement()); } protected void handleReveal(IModelDelta delta) { IModelDelta parentDelta = delta.getParentDelta(); if (parentDelta == null) { DebugUIPlugin.log(new Status(IStatus.ERROR, DebugUIPlugin.getUniqueIdentifier(), "Invalid viewer update: " + delta + ", in " + getPresentationContext().getId(), null )); //$NON-NLS-1$ //$NON-NLS-2$ return; } handleExpand(parentDelta); reveal(delta); cancelRestore(getViewerTreePath(delta), IModelDelta.REVEAL); } /** * Reveals the element pointed to by given delta. * @param delta Delta pointing to the element to reveal. */ private void reveal(IModelDelta delta) { int modelIndex = delta.getIndex(); IInternalTreeModelViewer treeViewer = getViewer(); TreePath elementPath = getViewerTreePath(delta); if (modelIndex >= 0) { TreePath parentPath = elementPath.getParentPath(); if (parentPath == null) { parentPath = TreePath.EMPTY; } int viewIndex = modelToViewIndex(parentPath, modelIndex); if (viewIndex >= 0) { if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("[reveal] replace(" + delta.getParentDelta().getElement() + ", (model) " + modelIndex + " (view) " + viewIndex + ", " + delta.getElement()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } treeViewer.replace(parentPath, viewIndex, delta.getElement()); } else { // Element is filtered - insert if filter state changed viewIndex = unfilterElement(parentPath, delta.getElement(), modelIndex); if (viewIndex < 0) { // insert did not complete return; } } // only move tree based on force flag and selection policy if ((delta.getFlags() & IModelDelta.FORCE) != 0 || treeViewer.overrideSelection(treeViewer.getSelection(), new TreeSelection(elementPath))) { /* * Bug 438724 - Save reveal parameters and redo reveal on * updatesComplete() in case intermediate content updates have * shifted the revealed element again. */ fRevealPath = parentPath; fRevealIndex = viewIndex; treeViewer.reveal(parentPath, viewIndex); } } } @Override public TreePath[] getParents(Object element) { return null; } @Override public void updateChildCount(TreePath treePath, int currentChildCount) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("updateChildCount(" + getElement(treePath) + ", " + currentChildCount + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } doUpdateChildCount(treePath); } @Override public void updateElement(TreePath parentPath, int viewIndex) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); int modelIndex = viewToModelIndex(parentPath, viewIndex); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("updateElement("+ getElement(parentPath) + ", " + viewIndex + ") > modelIndex = " + modelIndex); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } doUpdateElement(parentPath, modelIndex); } @Override public void updateHasChildren(TreePath path) { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); if (DebugUIPlugin.DEBUG_CONTENT_PROVIDER && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) { DebugUIPlugin.trace("updateHasChildren(" + getElement(path)); //$NON-NLS-1$ } doUpdateHasChildren(path); } /** * Schedules given update to be performed on the viewer. * Updates are queued up if they are completed in the same * UI cycle. * @param update Update to perform. */ void scheduleViewerUpdate(ViewerUpdateMonitor update) { Display display; Runnable updateJob = null; synchronized(this) { if (isDisposed()) { return; } display = getViewer().getDisplay(); fCompletedUpdates.add(update); if (fCompletedUpdatesRunnable == null) { fCompletedUpdatesRunnable = () -> { if (!isDisposed()) { performUpdates(); } }; updateJob = fCompletedUpdatesRunnable; } } if (updateJob != null) { if (Thread.currentThread() == display.getThread()) { performUpdates(); } else { fDelayedDoModelChangeJob.runDelayed(updateJob); } } } /** * Perform the updates pointed to by given array on the viewer. */ private void performUpdates() { Assert.isTrue( getViewer().getDisplay().getThread() == Thread.currentThread() ); List jobCompletedUpdates; synchronized(this) { if (isDisposed()) { return; } jobCompletedUpdates = fCompletedUpdates; fCompletedUpdatesRunnable = null; fCompletedUpdates = new ArrayList<>(); } // necessary to check if viewer is disposed try { for (int i = 0; i < jobCompletedUpdates.size(); i++) { ViewerUpdateMonitor completedUpdate = jobCompletedUpdates.get(i); if (!completedUpdate.isCanceled() && !isDisposed()) { IStatus status = completedUpdate.getStatus(); if (status == null || status.isOK()) { completedUpdate.performUpdate(); } } } } finally { updatesComplete(jobCompletedUpdates); } } }