/******************************************************************************* * Copyright (c) 2017, 2020 Ericsson, Draeger Auriga * * All rights reserved. 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 *******************************************************************************/ package org.eclipse.tracecompass.tmf.ui.viewers.tree; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang3.StringUtils; 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.jdt.annotation.NonNull; import org.eclipse.jface.viewers.AbstractTreeViewer; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxTreeViewer; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.tracecompass.common.core.log.TraceCompassLog; import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils.FlowScopeLog; import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils.FlowScopeLogBuilder; import org.eclipse.tracecompass.internal.tmf.core.model.filters.FetchParametersUtils; import org.eclipse.tracecompass.internal.tmf.ui.Activator; import org.eclipse.tracecompass.tmf.core.dataprovider.DataProviderManager; import org.eclipse.tracecompass.tmf.core.model.filters.TimeQueryFilter; import org.eclipse.tracecompass.tmf.core.model.timegraph.IElementResolver; import org.eclipse.tracecompass.tmf.core.model.tree.ITmfTreeDataModel; import org.eclipse.tracecompass.tmf.core.model.tree.ITmfTreeDataProvider; import org.eclipse.tracecompass.tmf.core.model.tree.TmfTreeDataModel; import org.eclipse.tracecompass.tmf.core.model.tree.TmfTreeModel; import org.eclipse.tracecompass.tmf.core.response.ITmfResponse; import org.eclipse.tracecompass.tmf.core.response.TmfModelResponse; import org.eclipse.tracecompass.tmf.core.signal.TmfDataModelSelectedSignal; import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler; import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager; import org.eclipse.tracecompass.tmf.core.signal.TmfTraceOpenedSignal; import org.eclipse.tracecompass.tmf.core.signal.TmfTraceSelectedSignal; import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace; import org.eclipse.tracecompass.tmf.core.trace.TmfTraceContext; import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager; import org.eclipse.tracecompass.tmf.ui.viewers.ILegendImageProvider2; import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.dialogs.MultiTreePatternFilter; import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.dialogs.TriStateFilteredCheckboxTree; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.primitives.Longs; /** * Abstract viewer for trees with checkboxes. This viewer gets his data from a * data provider. * * @since 6.0 * @author Loic Prieur-Drevon */ public abstract class AbstractSelectTreeViewer2 extends AbstractTmfTreeViewer { private static final @NonNull Logger LOGGER = TraceCompassLog.getLogger(AbstractSelectTreeViewer2.class); private static final AtomicInteger INSTANCE_ID_SEQUENCE = new AtomicInteger(0); /** Timeout between updates in the updateData thread **/ private static final long BUILD_UPDATE_TIMEOUT = 500; /** ID of the checked tree items in the map of data in {@link TmfTraceContext} */ private static final char SEP = ':'; private static final @NonNull String CHECKED_ELEMENTS = ".CHECKED_ELEMENTS"; //$NON-NLS-1$ private static final @NonNull String FILTER_STRING = ".FILTER_STRING"; //$NON-NLS-1$ private static final @NonNull String UPDATE_CONTENT_JOB_NAME = "AbstractSelectTreeViewer#updateContent Job"; //$NON-NLS-1$ private static final String FAILED_TO_SLEEP_PREFIX = "Failed to sleep the "; //$NON-NLS-1$ private static final String LOG_CATEGORY_SUFFIX = " Tree viewer"; //$NON-NLS-1$ private final String fId; private final int fInstanceId; private final @NonNull String fLogCategory; private static final ViewerComparator COMPARATOR = new ViewerComparator() { @Override public int compare(Viewer viewer, Object e1, Object e2) { TmfTreeViewerEntry entry1 = (TmfTreeViewerEntry) e1; TmfTreeViewerEntry entry2 = (TmfTreeViewerEntry) e2; String name1 = entry1.getName(); String name2 = entry2.getName(); Long longValue1 = Longs.tryParse(name1); Long longValue2 = Longs.tryParse(name2); return (longValue1 == null || longValue2 == null) ? name1.compareTo(name2) : longValue1.compareTo(longValue2); } }; private final class CheckStateChangedListener implements ICheckStateListener { @Override public void checkStateChanged(CheckStateChangedEvent event) { saveViewContext(); if (!fTreeListeners.isEmpty()) { Collection<ITmfTreeViewerEntry> entries = getCheckedViewerEntries(); for (ICheckboxTreeViewerListener listener : fTreeListeners) { listener.handleCheckStateChangedEvent(entries); } refresh(); } } } /** * Listener to select a range in other viewers when a cell of the segment * store table view is selected */ private class TreeViewerSelectionListener implements ISelectionChangedListener { @Override public void selectionChanged(SelectionChangedEvent event) { IStructuredSelection selection = event.getStructuredSelection(); Object entry = selection.getFirstElement(); if (entry instanceof TmfGenericTreeEntry) { ITmfTreeDataModel model = ((TmfGenericTreeEntry<?>) entry).getModel(); if (model instanceof IElementResolver) { Multimap<@NonNull String, @NonNull Object> metadata = ((IElementResolver) model).getMetadata(); if (!metadata.isEmpty()) { TmfSignalManager.dispatchSignal(new TmfDataModelSelectedSignal(AbstractSelectTreeViewer2.this, metadata)); } } } } } /** * Base class to provide the labels for the tree viewer. Views extending * this class typically need to override the getColumnText method if they * have more than one column to display. It also allows to change the font * and colors of the cells. * @since 6.0 */ protected class DataProviderTreeLabelProvider extends TreeLabelProvider { @Override public Image getColumnImage(Object element, int columnIndex) { if (columnIndex == fLegendIndex && element instanceof TmfGenericTreeEntry && isChecked(element)) { TmfGenericTreeEntry<TmfTreeDataModel> genericEntry = (TmfGenericTreeEntry<TmfTreeDataModel>) element; TmfTreeDataModel model = genericEntry.getModel(); if (model.hasRowModel()) { return getLegendImage(genericEntry.getModel().getId()); } } return null; } } private ILegendImageProvider2 fLegendImageProvider; private Set<ICheckboxTreeViewerListener> fTreeListeners = new HashSet<>(); private TriStateFilteredCheckboxTree fCheckboxTree; private final int fLegendIndex; /** * Constructor * * @param parent * Parent composite * @param checkboxTree * <code>TriStateFilteredTree</code> wrapping a * <code>CheckboxTreeViewer</code> * @param legendColumnIndex * index of the legend column (-1 if none) */ private AbstractSelectTreeViewer2(Composite parent, TriStateFilteredCheckboxTree checkboxTree, int legendIndex, String id) { super(parent, checkboxTree.getViewer()); TreeViewer treeViewer = checkboxTree.getViewer(); treeViewer.setAutoExpandLevel(AbstractTreeViewer.ALL_LEVELS); treeViewer.setComparator(COMPARATOR); if (treeViewer instanceof CheckboxTreeViewer) { ((CheckboxTreeViewer) treeViewer).addCheckStateListener(new CheckStateChangedListener()); } treeViewer.addSelectionChangedListener(new TreeViewerSelectionListener()); checkboxTree.getFilterControl().addModifyListener(e -> saveViewContext()); fCheckboxTree = checkboxTree; fLegendIndex = legendIndex; fId = id; fLogCategory = fId + LOG_CATEGORY_SUFFIX; fInstanceId = INSTANCE_ID_SEQUENCE.incrementAndGet(); setLabelProvider(new DataProviderTreeLabelProvider()); } /** * Constructor * * @param parent * Parent composite * @param legendIndex * index of the legend column (-1 if none) * @param id * {@link ITmfTreeDataProvider} ID */ public AbstractSelectTreeViewer2(Composite parent, int legendIndex, String id) { // Create the tree viewer with a filtered checkbox this(parent, new TriStateFilteredCheckboxTree(parent, SWT.MULTI | SWT.H_SCROLL | SWT.FULL_SELECTION, new MultiTreePatternFilter(), true, false), legendIndex, id); } /** * Tell the chart viewer to listen to changes in the tree viewer * * @param listener * Chart listening to changes in the tree's selected entries * @deprecated As of 6.0, there can be more than one listener for the tree, * use {@link #addTreeListener(ICheckboxTreeViewerListener)} */ @Deprecated public void setTreeListener(ICheckboxTreeViewerListener listener) { addTreeListener(listener); } /** * Add a listener to changes in the tree viewer * * @param listener * Listener for changes in the tree's selected entries * @since 6.0 */ public void addTreeListener(ICheckboxTreeViewerListener listener) { fTreeListeners.add(listener); } /** * Remove a listener from this tree viewer * * @param listener * The listener to remove * @since 6.0 */ public void removeTreeListener(ICheckboxTreeViewerListener listener) { fTreeListeners.remove(listener); } /** * Set the legend image provider (provider tree cells with an image). * * @param legendImageProvider * Provides an image legend associated with a name */ public void setLegendImageProvider(ILegendImageProvider2 legendImageProvider) { fLegendImageProvider = legendImageProvider; } /** * Return the checked state of an element * * @param element * the element * @return if the element is checked */ public boolean isChecked(Object element) { return fCheckboxTree.getChecked(element); } /** * Select previously checked entries when going back to trace. */ @Override protected void contentChanged(ITmfTreeViewerEntry rootEntry) { ITmfTrace trace = getTrace(); if (trace == null) { return; } TmfTraceContext ctx = TmfTraceManager.getInstance().getTraceContext(trace); Set<Long> ids = (Set<Long>) ctx.getData(getDataContextId(CHECKED_ELEMENTS)); if (ids != null && rootEntry != null) { List<ITmfTreeViewerEntry> checkedElements = new ArrayList<>(); checkEntries(ids, rootEntry, checkedElements); internalSetCheckedElements(checkedElements.toArray()); } Object filterString = ctx.getData(getDataContextId(FILTER_STRING)); if (filterString instanceof String) { fCheckboxTree.setFilterText((String) filterString); } else { fCheckboxTree.setFilterText(""); //$NON-NLS-1$ } getTreeViewer().refresh(); } /** * Checks all the passed elements and unchecks all the other. * * @param checkedElements * the elements to check */ protected void setCheckedElements(Object[] checkedElements) { internalSetCheckedElements(checkedElements); getTreeViewer().refresh(); } private void internalSetCheckedElements(Object[] checkedElements) { fCheckboxTree.setCheckedElements(checkedElements); saveViewContext(); for (ICheckboxTreeViewerListener listener : fTreeListeners) { listener.handleCheckStateChangedEvent(getCheckedViewerEntries()); } } private Collection<ITmfTreeViewerEntry> getCheckedViewerEntries() { Object[] checkedElements = fCheckboxTree.getCheckedElements(); return Lists.newArrayList(Iterables.filter(Arrays.asList(checkedElements), ITmfTreeViewerEntry.class)); } /** * Recursively find entries which were previously checked and check them again * by id. * * @param ids * Set of previously checked IDs * @param root * {@link ITmfTreeViewerEntry} to compare to the set of checked * entries * @param checkedElements * list of checked entries to which we add the root entry if it was * previously checked */ private void checkEntries(Set<Long> ids, ITmfTreeViewerEntry root, List<ITmfTreeViewerEntry> checkedElements) { if (root instanceof TmfGenericTreeEntry && ids.contains(((TmfGenericTreeEntry) root).getModel().getId())) { checkedElements.add(root); } for (ITmfTreeViewerEntry child : root.getChildren()) { checkEntries(ids, child, checkedElements); } } @Override @TmfSignalHandler public void traceOpened(TmfTraceOpenedSignal signal) { saveViewContext(); restorePatternFilter(signal.getTrace()); fCheckboxTree.setCheckedElements(new Object[0]); super.traceOpened(signal); } @Override @TmfSignalHandler public void traceSelected(TmfTraceSelectedSignal signal) { if (getTrace() != signal.getTrace()) { saveViewContext(); restorePatternFilter(signal.getTrace()); fCheckboxTree.setCheckedElements(new Object[0]); } super.traceSelected(signal); } @Override public void reset() { fCheckboxTree.setCheckedElements(new Object[0]); fCheckboxTree.setFilterText(""); //$NON-NLS-1$ super.reset(); } @Override protected void updateContent(long start, long end, boolean isSelection) { try (FlowScopeLog scope = new FlowScopeLogBuilder(LOGGER, Level.FINE, UPDATE_CONTENT_JOB_NAME) .setCategory(fLogCategory).build()) { ITmfTrace trace = getTrace(); if (trace == null) { return; } Job thread = new Job(UPDATE_CONTENT_JOB_NAME) { @Override public IStatus run(IProgressMonitor monitor) { try (FlowScopeLog runScope = new FlowScopeLogBuilder(LOGGER, Level.FINE, UPDATE_CONTENT_JOB_NAME + " run") //$NON-NLS-1$ .setParentScope(scope).build()) { ITmfTreeDataProvider<@NonNull ITmfTreeDataModel> provider = getProvider(trace); if (provider == null) { return Status.OK_STATUS; } Map<String, Object> parameters = getParameters(start, end, isSelection); if (parameters.isEmpty()) { return Status.OK_STATUS; } boolean isComplete = false; do { TmfModelResponse<@NonNull TmfTreeModel<@NonNull ITmfTreeDataModel>> response; try (FlowScopeLog iterScope = new FlowScopeLogBuilder(LOGGER, Level.FINE, UPDATE_CONTENT_JOB_NAME + " query") //$NON-NLS-1$ .setParentScope(scope).build()) { response = provider.fetchTree(parameters, monitor); TmfTreeModel<@NonNull ITmfTreeDataModel> model = response.getModel(); if (model != null) { updateTree(trace, start, end, model.getEntries()); } } ITmfResponse.Status status = response.getStatus(); if (status == ITmfResponse.Status.COMPLETED) { /* Model is complete, no need to request again the data provider */ isComplete = true; } else if (status == ITmfResponse.Status.FAILED || status == ITmfResponse.Status.CANCELLED) { /* Error occurred, return */ isComplete = true; } else { /** * Status is RUNNING. Sleeping current thread to wait before request data * provider again **/ try { Thread.sleep(BUILD_UPDATE_TIMEOUT); } catch (InterruptedException e) { /** * InterruptedException is throw by Thread.Sleep and we should retry querying * the data provider **/ runScope.addData(FAILED_TO_SLEEP_PREFIX + getName(), e); Thread.currentThread().interrupt(); return new Status(IStatus.ERROR, Activator.PLUGIN_ID, FAILED_TO_SLEEP_PREFIX + getName()); } } } while (!isComplete); return Status.OK_STATUS; } } }; thread.setSystem(true); thread.schedule(); } } private void updateTree(ITmfTrace trace, long start, long end, List<@NonNull ITmfTreeDataModel> model) { try (FlowScopeLog parentScope = new FlowScopeLogBuilder(LOGGER, Level.FINE, "AbstractSelectTreeViewer:TreeUpdateRequested" ) //$NON-NLS-1$ .setCategory(fLogCategory).build()) { final ITmfTreeViewerEntry newRootEntry = modelToTree(start, end, model); /* Set the input in main thread only if it didn't change */ if (newRootEntry != null) { Display.getDefault().asyncExec(() -> { try (FlowScopeLog scope = new FlowScopeLogBuilder(LOGGER, Level.FINE, "AbstractSelectTreeViewer:TreeUpdate").setParentScope(parentScope).build()) { //$NON-NLS-1$ if (!trace.equals(getTrace())) { return; } TreeViewer treeViewer = getTreeViewer(); if (treeViewer.getControl().isDisposed()) { return; } Object currentRootEntry = treeViewer.getInput(); if (!(currentRootEntry instanceof ITmfTreeViewerEntry) || !treeEquals(newRootEntry, (ITmfTreeViewerEntry) currentRootEntry)) { updateTreeUI(treeViewer, newRootEntry); } else { treeViewer.refresh(); } // FIXME should add a bit of padding for (TreeColumn column : treeViewer.getTree().getColumns()) { column.pack(); } } }); } } } /** * Recursively compare two trees, as the tree viewer requires a fast method, * {@link Object#hashCode()} for example. * * @param entry1 * one of the root tree entries to compare * @param entry2 * the other tree entry to compare * @return if the trees are equal */ private boolean treeEquals(ITmfTreeViewerEntry entry1, ITmfTreeViewerEntry entry2) { if (!Objects.equals(entry1.getName(), entry2.getName())) { return false; } List<@NonNull ? extends ITmfTreeViewerEntry> children1 = entry1.getChildren(); List<@NonNull ? extends ITmfTreeViewerEntry> children2 = entry2.getChildren(); if (children1.size() != children2.size()) { return false; } int size = children1.size(); for (int i = 0; i < size; i++) { if (!treeEquals(children1.get(i), children2.get(i))) { return false; } } return true; } /** * Save the checked entries' ID in the view context before changing trace and * check them again in the new tree. */ private void saveViewContext() { ITmfTrace previousTrace = getTrace(); if (previousTrace != null) { Object[] checkedElements = fCheckboxTree.getCheckedElements(); Set<Long> ids = new HashSet<>(); for (Object checkedElement : checkedElements) { if (checkedElement instanceof TmfGenericTreeEntry) { ids.add(((TmfGenericTreeEntry) checkedElement).getModel().getId()); } } Text filterControl = fCheckboxTree.getFilterControl(); String filterString = filterControl != null ? filterControl.getText() : null; TmfTraceManager.getInstance().updateTraceContext(previousTrace, builder -> builder.setData(getDataContextId(CHECKED_ELEMENTS), ids) .setData(getDataContextId(FILTER_STRING), filterString)); } } private @NonNull String getDataContextId(String baseKey) { return getClass().getName() + SEP + fInstanceId + baseKey; } private void restorePatternFilter(ITmfTrace trace) { if (trace == null) { fCheckboxTree.getPatternFilter().setPattern(null); return; } TmfTraceContext ctx = TmfTraceManager.getInstance().getTraceContext(trace); Object filterString = ctx.getData(getDataContextId(FILTER_STRING)); if (filterString instanceof String) { fCheckboxTree.getPatternFilter().setPattern((String) filterString); } else { fCheckboxTree.getPatternFilter().setPattern(null); } } /** * Get the legend image for a entry's ID * * @param id * the entry's unique ID * @return the correctly dimensioned image if there is a legend image provider * @since 6.0 */ protected Image getLegendImage(@NonNull Long id) { /* If the image height match the row height, row height will increment */ ILegendImageProvider2 legendImageProvider = fLegendImageProvider; int legendColumnIndex = fLegendIndex; if (legendImageProvider != null && legendColumnIndex >= 0) { Tree tree = getTreeViewer().getTree(); int imageWidth = tree.getColumn(legendColumnIndex).getWidth(); int imageHeight = tree.getItemHeight() - 1; if (imageHeight > 0 && imageWidth > 0) { return legendImageProvider.getLegendImage(imageHeight, imageWidth, id); } } return null; } @Override protected ITmfTreeViewerEntry updateElements(ITmfTrace trace, long start, long end, boolean isSelection) { throw new UnsupportedOperationException("This method should not be called for AbstractSelectTreeViewers"); //$NON-NLS-1$ } /** * Getter for the {@link ITmfTreeDataProvider} to query for this TreeViewer * * @param trace the trace * @return the relevant provider, if any * @since 4.0 */ protected ITmfTreeDataProvider<@NonNull ITmfTreeDataModel> getProvider(@NonNull ITmfTrace trace) { return DataProviderManager.getInstance().getDataProvider(trace, fId, ITmfTreeDataProvider.class); } @Override protected void initializeDataSource(@NonNull ITmfTrace trace) { getProvider(trace); } /** * Get the map to query the {@link ITmfTreeDataProvider} for the queried * parameters * * @param start * query start time * @param end * query end time * @param isSelection * if the query is a selection * @return the resulting query parameters * @since 5.0 */ protected @NonNull Map<String, Object> getParameters(long start, long end, boolean isSelection) { return FetchParametersUtils.timeQueryToMap(new TimeQueryFilter(Long.min(start, end), Long.max(start, end), 2)); } /** * Algorithm to convert a model (List of {@link TmfTreeDataModel}) to the tree. * * @param start * queried start time * @param end * queried end time * @param model * model to convert * @return the resulting {@link TmfTreeViewerEntry}. */ protected ITmfTreeViewerEntry modelToTree(long start, long end, List<ITmfTreeDataModel> model) { TmfTreeViewerEntry root = new TmfTreeViewerEntry(StringUtils.EMPTY); Map<Long, TmfTreeViewerEntry> map = new HashMap<>(); map.put(-1L, root); for (ITmfTreeDataModel entry : model) { TmfGenericTreeEntry<ITmfTreeDataModel> viewerEntry = new TmfGenericTreeEntry<>(entry); map.put(entry.getId(), viewerEntry); TmfTreeViewerEntry parent = map.get(entry.getParentId()); if (parent != null && !parent.getChildren().contains(viewerEntry)) { parent.addChild(viewerEntry); } } return root; } /** * Create a sortable column * * @param text * column label * @param comparator * comparator to sort {@link TmfGenericTreeEntry}s * @return the comparator */ protected static <T extends TmfGenericTreeEntry<? extends TmfTreeDataModel>> @NonNull TmfTreeColumnData createColumn(String text, Comparator<T> comparator) { TmfTreeColumnData column = new TmfTreeColumnData(text); column.setComparator(new ViewerComparator() { @Override public int compare(Viewer viewer, Object e1, Object e2) { return comparator.compare((T) e1, (T) e2); } }); return column; } /** * Get the full path of the entry, from the trace to itself, to query the * presentation provider. * * @param entry * entry whose legend needs to be resolved. * @return the relevant series name. * @deprecated The ID should be used to query the presentation provider * instead of the full path */ @Deprecated protected static @NonNull String getFullPath(TmfGenericTreeEntry<TmfTreeDataModel> entry) { StringBuilder path = new StringBuilder(entry.getName()); ITmfTreeViewerEntry parent = entry.getParent(); while (parent instanceof TmfGenericTreeEntry) { path.insert(0, parent.getName() + '/'); parent = parent.getParent(); } return path.toString(); } /** * Get the checkbox tree in this viewer. * * @return the checkbox tree. * @since 4.0 */ public TriStateFilteredCheckboxTree getTriStateFilteredCheckboxTree() { return fCheckboxTree; } }