| /******************************************************************************* |
| * Copyright (c) 2006, 2018 IBM Corporation 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: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.debug.internal.ui.viewers; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IAdaptable; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| 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.elements.adapters.AsynchronousDebugLabelAdapter; |
| import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxy; |
| import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxyFactory; |
| import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext; |
| import org.eclipse.debug.internal.ui.viewers.model.provisional.IStatusMonitor; |
| import org.eclipse.debug.internal.ui.viewers.provisional.AsynchronousContentAdapter; |
| import org.eclipse.debug.internal.ui.viewers.provisional.IAsynchronousContentAdapter; |
| import org.eclipse.debug.internal.ui.viewers.provisional.IAsynchronousLabelAdapter; |
| import org.eclipse.debug.internal.ui.viewers.provisional.IChildrenRequestMonitor; |
| import org.eclipse.debug.internal.ui.viewers.provisional.ILabelRequestMonitor; |
| import org.eclipse.jface.viewers.ViewerComparator; |
| import org.eclipse.jface.viewers.ViewerFilter; |
| |
| /** |
| * Model for an asynchronous viewer |
| * |
| * @since 3.2 |
| */ |
| public abstract class AsynchronousModel { |
| |
| private ModelNode fRoot; // root node |
| private Map<Object, ModelNode[]> fElementToNodes = new HashMap<>(); // map |
| // of |
| // element |
| // to |
| // corresponding |
| // tree |
| // nodes |
| // (list) |
| private Map<Object, IModelProxy> fModelProxies = new HashMap<>(); // map |
| // of |
| // installed |
| // model |
| // proxies, |
| // by |
| // element |
| private AsynchronousViewer fViewer; // viewer this model works for |
| private boolean fDisposed = false; // whether disposed |
| |
| static class EmptyContentAdapter extends AsynchronousContentAdapter { |
| |
| @Override |
| protected Object[] getChildren(Object parent, IPresentationContext context) throws CoreException { |
| return EMPTY; |
| } |
| |
| @Override |
| protected boolean hasChildren(Object element, IPresentationContext context) throws CoreException { |
| return false; |
| } |
| |
| @Override |
| protected boolean supportsPartId(String id) { |
| return true; |
| } |
| } |
| |
| protected IAsynchronousContentAdapter fEmptyContentAdapter = new EmptyContentAdapter(); |
| |
| /** |
| * List of requests currently being performed. |
| */ |
| private List<IStatusMonitor> fPendingUpdates = new ArrayList<>(); |
| |
| /** |
| * List of pending viewer updates |
| */ |
| private List<IStatusMonitor> fViewerUpdates = new ArrayList<>(); |
| |
| /** |
| * Constructs a new empty tree model |
| * |
| * @param viewer associated viewer |
| */ |
| public AsynchronousModel(AsynchronousViewer viewer) { |
| fViewer = viewer; |
| if (DebugUIPlugin.DEBUG_MODEL) { |
| StringBuilder buffer = new StringBuilder(); |
| buffer.append("MODEL CREATED for: "); //$NON-NLS-1$ |
| buffer.append(fViewer); |
| buffer.append(" ("); //$NON-NLS-1$ |
| buffer.append(this); |
| buffer.append(")"); //$NON-NLS-1$ |
| DebugUIPlugin.trace(buffer.toString()); |
| } |
| } |
| |
| /** |
| * Initializes this model. Called once after creation. |
| * |
| * @param root root element or <code>null</code> |
| */ |
| public void init(Object root) { |
| if (root != null) { |
| fRoot = new ModelNode(null, root); |
| mapElement(root, fRoot); |
| } |
| } |
| |
| protected AsynchronousViewer getViewer() { |
| return fViewer; |
| } |
| |
| /** |
| * Disposes this model |
| */ |
| public synchronized void dispose() { |
| if (DebugUIPlugin.DEBUG_MODEL) { |
| StringBuilder buffer = new StringBuilder(); |
| buffer.append("MODEL DISPOSED for: "); //$NON-NLS-1$ |
| buffer.append(fViewer); |
| buffer.append(" ("); //$NON-NLS-1$ |
| buffer.append(this); |
| buffer.append(")"); //$NON-NLS-1$ |
| DebugUIPlugin.trace(buffer.toString()); |
| } |
| fDisposed = true; |
| cancelPendingUpdates(); |
| disposeAllModelProxies(); |
| ModelNode rootNode = getRootNode(); |
| if (rootNode != null) { |
| rootNode.dispose(); |
| } |
| fElementToNodes.clear(); |
| } |
| |
| /** |
| * Returns whether this model has been disposed |
| * @return <code>true</code> if the model is disposed <code>false</code> otherwise |
| */ |
| public synchronized boolean isDisposed() { |
| return fDisposed; |
| } |
| |
| /** |
| * Cancels all pending update requests. |
| */ |
| protected synchronized void cancelPendingUpdates() { |
| Iterator<IStatusMonitor> updates = fPendingUpdates.iterator(); |
| while (updates.hasNext()) { |
| IStatusMonitor update = updates.next(); |
| updates.remove(); |
| update.setCanceled(true); |
| } |
| fPendingUpdates.clear(); |
| } |
| |
| /** |
| * Installs the model proxy for the given element into this viewer |
| * if not already installed. |
| * |
| * @param element element to install an update policy for |
| */ |
| public synchronized void installModelProxy(Object element) { |
| if (!fModelProxies.containsKey(element)) { |
| IModelProxyFactory modelProxyFactory = getModelProxyFactoryAdapter(element); |
| if (modelProxyFactory != null) { |
| final IModelProxy proxy = modelProxyFactory.createModelProxy(element, getPresentationContext()); |
| if (proxy != null) { |
| fModelProxies.put(element, proxy); |
| Job job = new Job("Model Proxy installed notification job") {//$NON-NLS-1$ |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| if (!monitor.isCanceled()) { |
| proxy.init(getPresentationContext()); |
| getViewer().modelProxyAdded(proxy); |
| proxy.installed(getViewer()); |
| } |
| return Status.OK_STATUS; |
| } |
| }; |
| job.setSystem(true); |
| job.schedule(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Uninstalls the model proxy installed for the given element, if any. |
| * |
| * @param element the element context |
| */ |
| protected synchronized void disposeModelProxy(Object element) { |
| IModelProxy proxy = fModelProxies.remove(element); |
| if (proxy != null) { |
| getViewer().modelProxyRemoved(proxy); |
| proxy.dispose(); |
| } |
| } |
| |
| /** |
| * Unintalls all model proxies installed for this model |
| */ |
| private void disposeAllModelProxies() { |
| synchronized(fModelProxies) { |
| for (IModelProxy proxy : fModelProxies.values()) { |
| getViewer().modelProxyRemoved(proxy); |
| proxy.dispose(); |
| } |
| fModelProxies.clear(); |
| } |
| } |
| |
| /** |
| * Returns the presentation this model is installed in |
| * |
| * @return the presentation context from the backing viewer |
| */ |
| protected IPresentationContext getPresentationContext() { |
| return fViewer.getPresentationContext(); |
| } |
| |
| /** |
| * Returns the model proxy factory for the given element of <code>null</code> if none. |
| * |
| * @param element element to retrieve adapters for |
| * @return model proxy factory adapter or <code>null</code> |
| */ |
| protected IModelProxyFactory getModelProxyFactoryAdapter(Object element) { |
| IModelProxyFactory adapter = null; |
| if (element instanceof IModelProxyFactory) { |
| adapter = (IModelProxyFactory) element; |
| } else if (element instanceof IAdaptable) { |
| IAdaptable adaptable = (IAdaptable) element; |
| adapter = adaptable.getAdapter(IModelProxyFactory.class); |
| } |
| return adapter; |
| } |
| |
| /** |
| * Maps the given element to the given node. |
| * |
| * @param element the element context |
| * @param node the model node |
| */ |
| protected synchronized void mapElement(Object element, ModelNode node) { |
| ModelNode[] nodes = getNodes(element); |
| node.remap(element); |
| if (nodes == null) { |
| fElementToNodes.put(element, new ModelNode[] { node}); |
| } else { |
| for (ModelNode mnode : nodes) { |
| if (mnode == node) { |
| return; |
| } |
| } |
| ModelNode[] old = nodes; |
| ModelNode[] newNodes = new ModelNode[old.length + 1]; |
| System.arraycopy(old, 0, newNodes, 0, old.length); |
| newNodes[old.length] = node; |
| fElementToNodes.put(element, newNodes); |
| } |
| installModelProxy(element); |
| } |
| |
| /** |
| * Unmaps the given node from its element and widget. |
| * |
| * @param node the model node |
| */ |
| protected synchronized void unmapNode(ModelNode node) { |
| Object element = node.getElement(); |
| ModelNode[] nodes = fElementToNodes.get(element); |
| if (nodes == null) { |
| return; |
| } |
| if (nodes.length == 1) { |
| fElementToNodes.remove(element); |
| disposeModelProxy(element); |
| } else { |
| for (int i = 0; i < nodes.length; i++) { |
| ModelNode node2 = nodes[i]; |
| if (node2 == node) { |
| ModelNode[] newNodes= new ModelNode[nodes.length - 1]; |
| System.arraycopy(nodes, 0, newNodes, 0, i); |
| if (i < newNodes.length) { |
| System.arraycopy(nodes, i + 1, newNodes, i, newNodes.length - i); |
| } |
| fElementToNodes.put(element, newNodes); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the nodes in this model for the given element or |
| * <code>null</code> if none. |
| * |
| * @param element model element |
| * @return associated nodes or <code>null</code> |
| */ |
| public synchronized ModelNode[] getNodes(Object element) { |
| return fElementToNodes.get(element); |
| } |
| |
| /** |
| * Returns the root node or <code>null</code> |
| * |
| * @return the root node or <code>null</code> |
| */ |
| public ModelNode getRootNode() { |
| return fRoot; |
| } |
| |
| /** |
| * Cancels any conflicting updates for children of the given item, and |
| * schedules the new update. |
| * |
| * @param update the update to schedule |
| */ |
| protected void requestScheduled(IStatusMonitor update) { |
| AsynchronousRequestMonitor absUpdate = (AsynchronousRequestMonitor) update; |
| synchronized (fPendingUpdates) { |
| ListIterator<IStatusMonitor> updates = fPendingUpdates.listIterator(); |
| while (updates.hasNext()) { |
| AsynchronousRequestMonitor pendingUpdate = (AsynchronousRequestMonitor) updates.next(); |
| if (absUpdate.contains(pendingUpdate)) { |
| updates.remove(); |
| pendingUpdate.setCanceled(true); |
| } |
| } |
| fPendingUpdates.add(update); |
| } |
| } |
| |
| /** |
| * Removes the update from the pending updates list. |
| * |
| * @param update the update to remove |
| */ |
| protected void requestComplete(IStatusMonitor update) { |
| synchronized (fPendingUpdates) { |
| fPendingUpdates.remove(update); |
| } |
| } |
| |
| /** |
| * An viewer update has been scheduled due to the following update request. |
| * |
| * @param update the update to add |
| */ |
| protected void viewerUpdateScheduled(IStatusMonitor update) { |
| // synch viewer updates and pending updates on same lock - fPendingUpdates |
| synchronized (fPendingUpdates) { |
| fViewerUpdates.add(update); |
| } |
| } |
| |
| /** |
| * Returns the result of running the given elements through the |
| * viewers filters. |
| * |
| * @param parent parent element |
| * @param elements the elements to filter |
| * @return only the elements which all filters accept |
| */ |
| protected Object[] filter(Object parent, Object[] elements) { |
| ViewerFilter[] filters = getViewer().getFilters(); |
| if (filters != null) { |
| ArrayList<Object> filtered = new ArrayList<>(elements.length); |
| for (Object element : elements) { |
| boolean add = true; |
| for (ViewerFilter filter : filters) { |
| add = filter.select(getViewer(), parent, element); |
| if (!add) { |
| break; |
| } |
| } |
| if (add) { |
| filtered.add(element); |
| } |
| } |
| return filtered.toArray(); |
| } |
| return elements; |
| } |
| |
| /** |
| * Refreshes the given node. |
| * |
| * @param node the model node to update |
| */ |
| protected void updateLabel(ModelNode node) { |
| Object element = node.getElement(); |
| IAsynchronousLabelAdapter adapter = getLabelAdapter(element); |
| if (adapter != null) { |
| ILabelRequestMonitor labelUpdate = new LabelRequestMonitor(node, this); |
| requestScheduled(labelUpdate); |
| adapter.retrieveLabel(element, getPresentationContext(), labelUpdate); |
| } |
| } |
| |
| /** |
| * Returns the label adapter for the given element or <code>null</code> if none. |
| * |
| * @param element element to retrieve adapter for |
| * @return presentation adapter or <code>null</code> |
| */ |
| protected IAsynchronousLabelAdapter getLabelAdapter(Object element) { |
| IAsynchronousLabelAdapter adapter = null; |
| if (element instanceof IAsynchronousLabelAdapter) { |
| adapter = (IAsynchronousLabelAdapter) element; |
| } else if (element instanceof IAdaptable) { |
| IAdaptable adaptable = (IAdaptable) element; |
| adapter = adaptable.getAdapter(IAsynchronousLabelAdapter.class); |
| } |
| // if no adapter, use default (i.e. model presentation) |
| if (adapter == null) { |
| return new AsynchronousDebugLabelAdapter(); |
| } |
| return adapter; |
| } |
| |
| /** |
| * Returns the tree element adapter for the given element or |
| * <code>null</code> if none. |
| * |
| * @param element |
| * element to retrieve adapter for |
| * @return presentation adapter or <code>null</code> |
| */ |
| protected IAsynchronousContentAdapter getContentAdapter(Object element) { |
| IAsynchronousContentAdapter adapter = null; |
| if (element instanceof IAsynchronousContentAdapter) { |
| adapter = (IAsynchronousContentAdapter) element; |
| } else if (element instanceof IAdaptable) { |
| IAdaptable adaptable = (IAdaptable) element; |
| adapter = adaptable.getAdapter(IAsynchronousContentAdapter.class); |
| } |
| return adapter; |
| } |
| |
| /** |
| * Updates the children of the given node. |
| * |
| * @param parent |
| * node of which to update children |
| */ |
| public void updateChildren(ModelNode parent) { |
| Object element = parent.getElement(); |
| IAsynchronousContentAdapter adapter = getContentAdapter(element); |
| if (adapter == null) { |
| adapter = fEmptyContentAdapter; |
| } |
| if (adapter != null) { |
| IChildrenRequestMonitor update = new ChildrenRequestMonitor(parent, this); |
| requestScheduled(update); |
| adapter.retrieveChildren(element, getPresentationContext(), update); |
| } |
| } |
| |
| /** |
| * Update this model's viewer preserving its selection. |
| * |
| * @param update the update code to run in the backing viewer's preserving selection method |
| */ |
| protected void preservingSelection(Runnable update) { |
| getViewer().preservingSelection(update); |
| } |
| |
| /** |
| * The viewer updated associated with a request is complete. |
| * |
| * @param monitor the status to remove and complete |
| */ |
| protected void viewerUpdateComplete(IStatusMonitor monitor) { |
| // synch viewer updates and pending updates on same lock - fPendingUpdates |
| synchronized (fPendingUpdates) { |
| fViewerUpdates.remove(monitor); |
| } |
| getViewer().updateComplete(monitor); |
| } |
| |
| /** |
| * An update request was cancelled |
| * |
| * @param monitor the monitor to remove |
| */ |
| protected void requestCanceled(AsynchronousRequestMonitor monitor) { |
| synchronized (fPendingUpdates) { |
| fPendingUpdates.remove(monitor); |
| } |
| } |
| |
| /** |
| * Whether any updates are still in progress in the model or against the viewer. |
| * |
| * @return <code>true</code> if there are pending changes <code>false</code> otherwise |
| */ |
| protected boolean hasPendingUpdates() { |
| synchronized (fViewerUpdates) { |
| return !fPendingUpdates.isEmpty() || !fViewerUpdates.isEmpty(); |
| } |
| } |
| |
| /** |
| * Asynchronous update for add/set children request. |
| * |
| * @param parent the parent model node |
| * @param element the element context |
| */ |
| protected abstract void add(ModelNode parent, Object element); |
| |
| /** |
| * Notification from children request monitor |
| * |
| * @param parentNode parent node |
| * @param kids list of model elements |
| */ |
| protected void setChildren(final ModelNode parentNode, List<Object> kids) { |
| final Object[] children = filter(parentNode.getElement(), kids.toArray()); |
| final AsynchronousViewer viewer = getViewer(); |
| ViewerComparator comparator = viewer.getComparator(); |
| if (comparator != null) { |
| comparator.sort(viewer, children); |
| } |
| |
| ModelNode[] prevKids = null; |
| ModelNode[] newChildren = null; |
| ModelNode[] unmap = null; |
| |
| synchronized (this) { |
| if (isDisposed()) { |
| return; |
| } |
| prevKids = parentNode.getChildrenNodes(); |
| if (prevKids == null) { |
| newChildren = new ModelNode[children.length]; |
| for (int i = 0; i < children.length; i++) { |
| ModelNode node = new ModelNode(parentNode, children[i]); |
| mapElement(children[i], node); |
| newChildren[i] = node; |
| } |
| parentNode.setChildren(newChildren); |
| } else { |
| newChildren = new ModelNode[children.length]; |
| unmap = new ModelNode[prevKids.length]; |
| for (int i = 0; i < prevKids.length; i++) { |
| unmap[i] = prevKids[i]; |
| } |
| for (int i = 0; i < children.length; i++) { |
| Object child = children[i]; |
| boolean found = false; |
| for (int j = 0; j < prevKids.length; j++) { |
| ModelNode prevKid = prevKids[j]; |
| if (prevKid != null && child.equals(prevKid.getElement())) { |
| newChildren[i] = prevKid; |
| prevKids[j] = null; |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| newChildren[i] = new ModelNode(parentNode, child); |
| mapElement(child, newChildren[i]); |
| } |
| } |
| for (ModelNode kid : prevKids) { |
| if (kid != null) { |
| kid.dispose(); |
| unmapNode(kid); |
| } |
| } |
| parentNode.setChildren(newChildren); |
| } |
| if (DebugUIPlugin.DEBUG_MODEL) { |
| DebugUIPlugin.trace("CHILDREN CHANGED: " + parentNode); //$NON-NLS-1$ |
| DebugUIPlugin.trace(toString()); |
| } |
| } |
| |
| //update viewer outside the lock |
| final ModelNode[] finalUnmap = unmap; |
| preservingSelection(() -> { |
| if (finalUnmap != null) { |
| for (ModelNode element : finalUnmap) { |
| viewer.unmapNode(element); |
| } |
| } |
| viewer.nodeChildrenChanged(parentNode); |
| }); |
| |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder buf = new StringBuilder(); |
| if (fRoot != null) { |
| buf.append("ROOT: "); //$NON-NLS-1$ |
| append(buf, fRoot, 0); |
| } else { |
| buf.append("ROOT: null"); //$NON-NLS-1$ |
| } |
| return buf.toString(); |
| } |
| |
| private void append(StringBuilder buf, ModelNode node, int level) { |
| for (int i = 0; i < level; i++) { |
| buf.append('\t'); |
| } |
| buf.append(node); |
| buf.append('\n'); |
| ModelNode[] childrenNodes = node.getChildrenNodes(); |
| if (childrenNodes != null) { |
| for (ModelNode childNode : childrenNodes) { |
| append(buf, childNode, level + 1); |
| } |
| } |
| } |
| } |