blob: ab4286224f1e7f46b06db28d531b8b3f915cb054 [file] [log] [blame]
/*******************************************************************************
* 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
* Wind River - Pawel Piech - Added coalescing of label updates (bug 247575).
* Pawel Piech (Wind River) - added support for a virtual tree model viewer (Bug 242489)
*******************************************************************************/
package org.eclipse.debug.internal.ui.viewers.model;
import java.util.ArrayList;
import java.util.HashMap;
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.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.IElementLabelProvider;
import org.eclipse.debug.internal.ui.viewers.model.provisional.ILabelUpdate;
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.IModelDeltaVisitor;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelProxy;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
/**
* @since 3.3
*/
public class TreeModelLabelProvider extends ColumnLabelProvider
implements ITreeModelLabelProvider, IModelChangedListener
{
private IInternalTreeModelViewer fViewer;
/**
* Note: access this variable should be synchronized with <code>this</code>.
*/
private List<ILabelUpdate> fComplete;
/**
* Cache of images used for elements in this label provider. Label updates
* use the method <code>getImage(...)</code> to cache images for
* image descriptors. The images are disposed with this label provider.
*/
private Map<ImageDescriptor, Image> fImageCache = new HashMap<>();
/**
* Cache of the fonts used for elements in this label provider. Label updates
* use the method <code>getFont(...)</code> to cache fonts for
* FontData objects. The fonts are disposed with this label provider.
*/
private Map<FontData, Font> fFontCache = new HashMap<>();
/**
* Cache of the colors used for elements in this label provider. Label updates
* use the method <code>getColor(...)</code> to cache colors for
* RGB values. The colors are disposed with this label provider.
*/
private Map<RGB, Color> fColorCache = new HashMap<>();
/**
* Label listeners
*/
private ListenerList<ILabelUpdateListener> fLabelListeners = new ListenerList<>();
/**
* Updates waiting to be sent to the label provider. The map contains
* lists of updates, keyed using the provider.
*/
private Map<IElementLabelProvider, List<ILabelUpdate>> fPendingUpdates = new HashMap<>();
/**
* A runnable that will send the label update requests.
* This variable allows the job to be canceled and re-scheduled if
* new updates are requested.
*/
private Runnable fPendingUpdatesRunnable;
/**
* List of updates in progress
*/
private List<ILabelUpdate> fUpdatesInProgress = new ArrayList<>();
/**
* Delta visitor actively cancels the outstanding label updates for
* elements that are changed and are about to be updated.
*/
class CancelPendingUpdatesVisitor implements IModelDeltaVisitor {
/* (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) {
if ((delta.getFlags() & IModelDelta.CONTENT) > 0) {
cancelElementUpdates(delta.getElement(), true);
return false;
} else if ((delta.getFlags() & IModelDelta.STATE) > 0) {
cancelElementUpdates(delta.getElement(), false);
return true;
}
return true;
}
}
/**
* Delta visitor
*/
private CancelPendingUpdatesVisitor fCancelPendingUpdatesVisitor = new CancelPendingUpdatesVisitor();
/**
* Constructs a new label provider on the given display
* @param viewer Viewer that this label provider is used with.
*/
public TreeModelLabelProvider(IInternalTreeModelViewer viewer) {
fViewer = viewer;
fViewer.addModelChangedListener(this);
}
/**
* Returns an image for the given image descriptor or <code>null</code>. Adds the image
* to a cache of images if it does not already exist.
*
* @param descriptor image descriptor or <code>null</code>
* @return image or <code>null</code>
*/
@Override
public Image getImage(ImageDescriptor descriptor) {
if (descriptor == null) {
return null;
}
Image image = fImageCache.get(descriptor);
if (image == null) {
image = new Image(getDisplay(), descriptor.getImageData());
fImageCache.put(descriptor, image);
}
return image;
}
/**
* Returns the display to use for resource allocation.
*
* @return display
*/
private Display getDisplay() {
return fViewer.getDisplay();
}
/**
* Returns a font for the given font data or <code>null</code>. Adds the font to the font
* cache if not yet created.
*
* @param fontData font data or <code>null</code>
* @return font font or <code>null</code>
*/
@Override
public Font getFont(FontData fontData) {
if (fontData == null) {
return null;
}
Font font = fFontCache.get(fontData);
if (font == null) {
font = new Font(getDisplay(), fontData);
fFontCache.put(fontData, font);
}
return font;
}
/**
* Returns a color for the given RGB or <code>null</code>. Adds the color to the color
* cache if not yet created.
*
* @param rgb RGB or <code>null</code>
* @return color or <code>null</code>
*/
@Override
public Color getColor(RGB rgb) {
if (rgb == null) {
return null;
}
Color color = fColorCache.get(rgb);
if (color == null) {
color = new Color(getDisplay(), rgb);
fColorCache.put(rgb, color);
}
return color;
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.BaseLabelProvider#dispose()
*/
@Override
public void dispose() {
Assert.isTrue(fViewer.getDisplay().getThread() == Thread.currentThread());
fViewer.removeModelChangedListener(this);
fViewer = null;
List<ILabelUpdate> complete = null;
synchronized(this) {
complete = fComplete;
fComplete = null;
}
if (complete != null) {
for (ILabelUpdate update : complete) {
update.cancel();
}
}
for (ILabelUpdate currentUpdate : fUpdatesInProgress) {
currentUpdate.cancel();
}
if (fPendingUpdatesRunnable != null) {
fPendingUpdatesRunnable = null;
}
for (List<ILabelUpdate> updateList : fPendingUpdates.values()) {
for (ILabelUpdate update : updateList) {
update.cancel();
}
}
fPendingUpdates.clear();
for (Image image : fImageCache.values()) {
image.dispose();
}
fImageCache.clear();
for (Font font : fFontCache.values()) {
font.dispose();
}
fFontCache.clear();
for (Color color : fColorCache.values()) {
color.dispose();
}
fColorCache.clear();
super.dispose();
}
private boolean isDisposed() {
return fViewer == null;
}
@Override
public void update(ViewerCell cell) {
// NOT USED - the viewer updates each row instead
}
@Override
public boolean update(TreePath elementPath) {
Assert.isTrue(fViewer.getDisplay().getThread() == Thread.currentThread());
cancelPathUpdates(elementPath);
String[] visibleColumns = fViewer.getVisibleColumns();
Object element = elementPath.getLastSegment();
IElementLabelProvider presentation = ViewerAdapterService.getLabelProvider(element);
if (presentation != null) {
List<ILabelUpdate> updates = fPendingUpdates.get(presentation);
if (updates == null) {
updates = new LinkedList<>();
fPendingUpdates.put(presentation, updates);
}
updates.add(new LabelUpdate(fViewer.getInput(), elementPath, this, visibleColumns, fViewer.getPresentationContext()));
fPendingUpdatesRunnable = new Runnable() {
@Override
public void run() {
if (isDisposed()) {
return;
}
startRequests(this);
}
};
fViewer.getDisplay().asyncExec(fPendingUpdatesRunnable);
return true;
} else {
return false;
}
}
/**
* Cancel any outstanding updates that are running for this element.
* @param elementPath Element to cancel updates for.
*/
private void cancelPathUpdates(TreePath elementPath) {
Assert.isTrue(fViewer.getDisplay().getThread() == Thread.currentThread());
for (ILabelUpdate currentUpdate : fUpdatesInProgress) {
if (elementPath.equals(currentUpdate.getElementPath())) {
currentUpdate.cancel();
}
}
}
/**
* Sets the element's display information in the viewer.
*
* @param path Element path.
* @param numColumns Number of columns in the data.
* @param labels Array of labels. The array cannot to be
* <code>null</code>, but values within the array may be.
* @param images Array of image descriptors, may be <code>null</code>.
* @param fontDatas Array of fond data objects, may be <code>null</code>.
* @param foregrounds Array of RGB values for foreground colors, may be
* <code>null</code>.
* @param backgrounds Array of RGB values for background colors, may be
* <code>null</code>.
* @param checked Whether given item should be checked.
* @param grayed Whether given item's checkbox should be grayed.
*/
void setElementData(TreePath path, int numColumns, String[] labels, ImageDescriptor[] images,
FontData[] fontDatas, RGB[] foregrounds, RGB[] backgrounds, boolean checked, boolean grayed)
{
fViewer.setElementData(path, numColumns, labels, images, fontDatas, foregrounds, backgrounds);
fViewer.setElementChecked(path, checked, grayed);
}
private void startRequests(Runnable runnable) {
if (runnable != fPendingUpdatesRunnable) {
return;
}
if (!fPendingUpdates.isEmpty()) {
List<ILabelUpdate> list = null;
for (Entry<IElementLabelProvider, List<ILabelUpdate>> entry : fPendingUpdates.entrySet()) {
list = entry.getValue();
for (ILabelUpdate update : list) {
updateStarted(update);
}
entry.getKey().update(list.toArray(new ILabelUpdate[list.size()]));
}
}
fPendingUpdates.clear();
fPendingUpdatesRunnable = null;
}
/**
* Cancels all running updates for the given element. If seachFullPath is true,
* all updates will be canceled which have the given element anywhere in their
* patch.
* @param element element to search for.
* @param searchFullPath flag whether to look for the element in the full path
* of the update
*/
private void cancelElementUpdates(Object element, boolean searchFullPath) {
for (ILabelUpdate currentUpdate : fUpdatesInProgress) {
if (searchFullPath) {
if (element.equals(fViewer.getInput())) {
currentUpdate.cancel();
} else {
TreePath updatePath = currentUpdate.getElementPath();
for (int i = 0; i < updatePath.getSegmentCount(); i++) {
if (element.equals(updatePath.getSegment(i))) {
currentUpdate.cancel();
break; // Exit the for loop, stay in the while loop
}
}
}
} else {
if (element.equals(currentUpdate.getElement())) {
currentUpdate.cancel();
}
}
}
}
/**
* Returns the presentation context for this label provider.
*
* @return presentation context
*/
private IPresentationContext getPresentationContext() {
return fViewer.getPresentationContext();
}
/**
* A label update is complete.
*
* @param update Update that is to be completed.
*/
synchronized void complete(ILabelUpdate update) {
if (fViewer == null) {
return;
}
if (fComplete == null) {
fComplete = new LinkedList<>();
fViewer.getDisplay().asyncExec(() -> {
if (isDisposed()) {
return;
}
List<ILabelUpdate> updates = null;
synchronized (TreeModelLabelProvider.this) {
updates = fComplete;
fComplete = null;
}
for (ILabelUpdate itrUpdate : updates) {
if (itrUpdate.isCanceled()) {
updateComplete(itrUpdate);
} else {
((LabelUpdate) itrUpdate).performUpdate();
}
}
});
}
fComplete.add(update);
}
@Override
public void addLabelUpdateListener(ILabelUpdateListener listener) {
fLabelListeners.add(listener);
}
@Override
public void removeLabelUpdateListener(ILabelUpdateListener listener) {
fLabelListeners.remove(listener);
}
/**
* Notification an update request has started
*
* @param update Update that was started
*/
void updateStarted(ILabelUpdate update) {
Assert.isTrue(fViewer.getDisplay().getThread() == Thread.currentThread());
boolean begin = fUpdatesInProgress.isEmpty();
fUpdatesInProgress.add(update);
if (begin) {
if (DebugUIPlugin.DEBUG_UPDATE_SEQUENCE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) {
DebugUIPlugin.trace("LABEL SEQUENCE BEGINS"); //$NON-NLS-1$
}
notifyUpdate(TreeModelContentProvider.UPDATE_SEQUENCE_BEGINS, null);
}
if (DebugUIPlugin.DEBUG_UPDATE_SEQUENCE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) {
DebugUIPlugin.trace("\tBEGIN - " + update); //$NON-NLS-1$
}
notifyUpdate(TreeModelContentProvider.UPDATE_BEGINS, update);
}
/**
* Notification an update request has completed
*
* @param update Update that completed.
*/
void updateComplete(ILabelUpdate update) {
fUpdatesInProgress.remove(update);
if (DebugUIPlugin.DEBUG_UPDATE_SEQUENCE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) {
DebugUIPlugin.trace("\tEND - " + update); //$NON-NLS-1$
}
notifyUpdate(TreeModelContentProvider.UPDATE_COMPLETE, update);
if (fUpdatesInProgress.isEmpty()) {
if (DebugUIPlugin.DEBUG_UPDATE_SEQUENCE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(getPresentationContext())) {
DebugUIPlugin.trace("LABEL SEQUENCE ENDS"); //$NON-NLS-1$
}
notifyUpdate(TreeModelContentProvider.UPDATE_SEQUENCE_COMPLETE, null);
}
}
private void notifyUpdate(final int type, final ILabelUpdate update) {
if (!fLabelListeners.isEmpty()) {
for (ILabelUpdateListener iLabelUpdateListener : fLabelListeners) {
final ILabelUpdateListener listener = iLabelUpdateListener;
SafeRunner.run(new ISafeRunnable() {
@Override
public void run() throws Exception {
switch (type) {
case TreeModelContentProvider.UPDATE_SEQUENCE_BEGINS:
listener.labelUpdatesBegin();
break;
case TreeModelContentProvider.UPDATE_SEQUENCE_COMPLETE:
listener.labelUpdatesComplete();
break;
case TreeModelContentProvider.UPDATE_BEGINS:
listener.labelUpdateStarted(update);
break;
case TreeModelContentProvider.UPDATE_COMPLETE:
listener.labelUpdateComplete(update);
break;
default:
break;
}
}
@Override
public void handleException(Throwable exception) {
DebugUIPlugin.log(exception);
}
});
}
}
}
@Override
public void modelChanged(IModelDelta delta, IModelProxy proxy) {
delta.accept(fCancelPendingUpdatesVisitor);
}
}