/*******************************************************************************
* Copyright (c) 2014 École Polytechnique de Montréal
*
* 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:
* Geneviève Bastien - Initial API and implementation
*******************************************************************************/
package org.eclipse.tracecompass.tmf.ui.viewers.tree;
import java.util.List;
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.IBaseLabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableColorProvider;
import org.eclipse.jface.viewers.ITableFontProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.tracecompass.tmf.core.signal.TmfSelectionRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfWindowRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.ui.viewers.TmfTimeViewer;
/**
* Abstract class for viewers who will display data using a TreeViewer. It
* automatically synchronizes with time information of the UI. It also
* implements some common functionalities for all tree viewer, such as managing
* the column data, content initialization and update. The viewer implementing
* this does not have to worry about whether some code runs in the UI thread or
* not.
*
* @author Geneviève Bastien
*/
public abstract class AbstractTmfTreeViewer extends TmfTimeViewer {
private final TreeViewer fTreeViewer;
// ------------------------------------------------------------------------
// Internal classes
// ------------------------------------------------------------------------
/* The elements of the tree viewer are of type ITmfTreeViewerEntry */
private class TreeContentProvider implements ITreeContentProvider {
@Override
public void dispose() {
}
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
}
@Override
public Object[] getElements(Object inputElement) {
if (inputElement instanceof ITmfTreeViewerEntry) {
return ((ITmfTreeViewerEntry) inputElement).getChildren().toArray(new ITmfTreeViewerEntry[0]);
}
return new ITmfTreeViewerEntry[0];
}
@Override
public Object[] getChildren(Object parentElement) {
ITmfTreeViewerEntry entry = (ITmfTreeViewerEntry) parentElement;
List<? extends ITmfTreeViewerEntry> children = entry.getChildren();
return children.toArray(new ITmfTreeViewerEntry[children.size()]);
}
@Override
public Object getParent(Object element) {
ITmfTreeViewerEntry entry = (ITmfTreeViewerEntry) element;
return entry.getParent();
}
@Override
public boolean hasChildren(Object element) {
ITmfTreeViewerEntry entry = (ITmfTreeViewerEntry) element;
return entry.hasChildren();
}
}
/**
* 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.
*/
protected static class TreeLabelProvider implements ITableLabelProvider, ITableFontProvider, ITableColorProvider {
@Override
public void addListener(ILabelProviderListener listener) {
}
@Override
public void dispose() {
}
@Override
public boolean isLabelProperty(Object element, String property) {
return false;
}
@Override
public void removeListener(ILabelProviderListener listener) {
}
@Override
public Image getColumnImage(Object element, int columnIndex) {
return null;
}
@Override
public String getColumnText(Object element, int columnIndex) {
if ((element instanceof ITmfTreeViewerEntry) && (columnIndex == 0)) {
ITmfTreeViewerEntry entry = (ITmfTreeViewerEntry) element;
return entry.getName();
}
return new String();
}
@Override
public Color getForeground(Object element, int columnIndex) {
return Display.getCurrent().getSystemColor(SWT.COLOR_LIST_FOREGROUND);
}
@Override
public Color getBackground(Object element, int columnIndex) {
return Display.getCurrent().getSystemColor(SWT.COLOR_LIST_BACKGROUND);
}
@Override
public Font getFont(Object element, int columnIndex) {
return null;
}
}
// ------------------------------------------------------------------------
// Constructors and initialization methods
// ------------------------------------------------------------------------
/**
* Constructor
*
* @param parent
* The parent composite that holds this viewer
* @param allowMultiSelect
* Whether multiple selections are allowed
*/
public AbstractTmfTreeViewer(Composite parent, boolean allowMultiSelect) {
super(parent);
int flags = SWT.FULL_SELECTION | SWT.H_SCROLL;
if (allowMultiSelect) {
flags |= SWT.MULTI;
}
/* Build the tree viewer part of the view */
fTreeViewer = new TreeViewer(parent, flags);
fTreeViewer.setAutoExpandLevel(AbstractTreeViewer.ALL_LEVELS);
final Tree tree = fTreeViewer.getTree();
tree.setHeaderVisible(true);
tree.setLinesVisible(true);
fTreeViewer.setContentProvider(new TreeContentProvider());
fTreeViewer.setLabelProvider(new TreeLabelProvider());
List<TmfTreeColumnData> columns = getColumnDataProvider().getColumnData();
this.setTreeColumns(columns);
}
/**
* Get the column data provider that will contain the list of columns to be
* part of this viewer. It is called once during the constructor.
*
* @return The tree column data provider for this viewer.
*/
protected abstract ITmfTreeColumnDataProvider getColumnDataProvider();
/**
* Sets the tree columns for this tree viewer
*
* @param columns
* The tree column data
*/
public void setTreeColumns(final List<TmfTreeColumnData> columns) {
boolean hasPercentProvider = false;
for (final TmfTreeColumnData columnData : columns) {
columnData.createColumn(fTreeViewer);
hasPercentProvider |= (columnData.getPercentageProvider() != null);
}
if (hasPercentProvider) {
/*
* Handler that will draw bar charts in the cell using a percentage
* value.
*/
fTreeViewer.getTree().addListener(SWT.EraseItem, new Listener() {
@Override
public void handleEvent(Event event) {
if (columns.get(event.index).getPercentageProvider() != null) {
double percentage = columns.get(event.index).getPercentageProvider().getPercentage(event.item.getData());
if (percentage == 0) { // No bar to draw
return;
}
if ((event.detail & SWT.SELECTED) > 0) {
/*
* The item is selected. Draw our own background to
* avoid overwriting the bar.
*/
event.gc.fillRectangle(event.x, event.y, event.width, event.height);
event.detail &= ~SWT.SELECTED;
}
int barWidth = (int) ((fTreeViewer.getTree().getColumn(event.index).getWidth() - 8) * percentage);
int oldAlpha = event.gc.getAlpha();
Color oldForeground = event.gc.getForeground();
Color oldBackground = event.gc.getBackground();
/*
* Draws a transparent gradient rectangle from the color
* of foreground and background.
*/
event.gc.setAlpha(64);
event.gc.setForeground(event.item.getDisplay().getSystemColor(SWT.COLOR_BLUE));
event.gc.setBackground(event.item.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
event.gc.fillGradientRectangle(event.x, event.y, barWidth, event.height, true);
event.gc.drawRectangle(event.x, event.y, barWidth, event.height);
/* Restores old values */
event.gc.setForeground(oldForeground);
event.gc.setBackground(oldBackground);
event.gc.setAlpha(oldAlpha);
event.detail &= ~SWT.BACKGROUND;
}
}
});
}
}
/**
* Set the label provider that will fill the columns of the tree viewer
*
* @param labelProvider
* The label provider to fill the columns
*/
protected void setLabelProvider(IBaseLabelProvider labelProvider) {
fTreeViewer.setLabelProvider(labelProvider);
}
/**
* Get the tree viewer object
*
* @return The tree viewer object displayed by this viewer
* @since 2.2
*/
public TreeViewer getTreeViewer() {
return fTreeViewer;
}
// ------------------------------------------------------------------------
// ITmfViewer
// ------------------------------------------------------------------------
@Override
public Control getControl() {
return fTreeViewer.getControl();
}
@Override
public void refresh() {
Tree tree = fTreeViewer.getTree();
tree.setRedraw(false);
fTreeViewer.refresh();
fTreeViewer.expandAll();
tree.setRedraw(true);
}
@Override
public void loadTrace(ITmfTrace trace) {
super.loadTrace(trace);
Thread thread = new Thread() {
@Override
public void run() {
initializeDataSource();
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
clearContent();
updateContent(getWindowStartTime(), getWindowEndTime(), false);
}
});
}
};
thread.start();
}
// ------------------------------------------------------------------------
// Operations
// ------------------------------------------------------------------------
/**
* Set the currently selected items in the treeviewer
*
* @param selection
* The list of selected items
*/
public void setSelection(@NonNull List<ITmfTreeViewerEntry> selection) {
IStructuredSelection sel = new StructuredSelection(selection);
fTreeViewer.setSelection(sel, true);
}
/**
* Add a selection listener to the tree viewer. This will be called when the
* selection changes and contain all the selected items.
*
* The selection change listener can be used like this:
*
* <pre>
* getTreeViewer().addSelectionChangeListener(new ISelectionChangedListener() {
* @Override
* public void selectionChanged(SelectionChangedEvent event) {
* if (event.getSelection() instanceof IStructuredSelection) {
* Object selection = ((IStructuredSelection) event.getSelection()).getFirstElement();
* if (selection instanceof ITmfTreeViewerEntry) {
* // Do something
* }
* }
* }
* });
* </pre>
*
* @param listener
* The {@link ISelectionChangedListener}
*/
public void addSelectionChangeListener(ISelectionChangedListener listener) {
fTreeViewer.addSelectionChangedListener(listener);
}
/**
* Method called when the trace is loaded, to initialize any data once the
* trace has been set, but before the first call to update the content of
* the viewer.
*/
protected void initializeDataSource() {
}
/**
* Clears the current content of the viewer.
*/
protected void clearContent() {
fTreeViewer.setInput(null);
}
/**
* Method called after the content has been updated and the new input has
* been set on the tree.
*
* @param rootEntry
* The new input of this viewer, or null if none
*/
protected void contentChanged(ITmfTreeViewerEntry rootEntry) {
}
/**
* Requests an update of the viewer's content in a given time range or
* selection time range. An extra parameter defines whether these times
* correspond to the selection or the visible range, as the viewer may
* update differently in those cases.
*
* @param start
* The start time of the requested content
* @param end
* The end time of the requested content
* @param isSelection
* <code>true</code> if this time range is for a selection,
* <code>false</code> for the visible time range
*/
protected void updateContent(final long start, final long end, final boolean isSelection) {
Job thread = new Job("") { //$NON-NLS-1$
@Override
public IStatus run(IProgressMonitor monitor) {
final ITmfTreeViewerEntry rootEntry = updateElements(start, end, isSelection);
/* Set the input in main thread only if it didn't change */
if (rootEntry != null) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
if (fTreeViewer.getControl().isDisposed()) {
return;
}
if (rootEntry != fTreeViewer.getInput()) {
fTreeViewer.setInput(rootEntry);
contentChanged(rootEntry);
} else {
fTreeViewer.refresh();
fTreeViewer.expandToLevel(fTreeViewer.getAutoExpandLevel());
}
// FIXME should add a bit of padding
for (TreeColumn column : fTreeViewer.getTree().getColumns()) {
column.pack();
}
}
});
}
return Status.OK_STATUS;
}
};
thread.setSystem(true);
thread.schedule();
}
/**
* Update the entries to the given start/end time. An extra parameter
* defines whether these times correspond to the selection or the visible
* range, as the viewer may update differently in those cases. This methods
* returns a root node that is not meant to be visible. The children of this
* 'fake' root node are the first level of entries that will appear in the
* tree. If no update is necessary, the method should return
* <code>null</code>. To empty the tree, a root node containing an empty
* list of children should be returned.
*
* This method is not called in the UI thread when using the default viewer
* content update. Resource-intensive calculations here should not block the
* UI.
*
* @param start
* The start time of the requested content
* @param end
* The end time of the requested content
* @param isSelection
* <code>true</code> if this time range is for a selection,
* <code>false</code> for the visible time range
* @return The root entry of the list of entries to display or
* <code>null</code> if no update necessary
*/
protected abstract ITmfTreeViewerEntry updateElements(long start, long end, boolean isSelection);
/**
* Get the current input displayed by the viewer
*
* @return The input of the tree viewer, the root entry
*/
protected ITmfTreeViewerEntry getInput() {
return (ITmfTreeViewerEntry) fTreeViewer.getInput();
}
// ------------------------------------------------------------------------
// Signal Handler
// ------------------------------------------------------------------------
/**
* Signal handler for handling of the time synch signal. The times
* correspond to the selection by the user, not the visible time range.
*
* @param signal
* The time synch signal {@link TmfSelectionRangeUpdatedSignal}
*/
@Override
@TmfSignalHandler
public void selectionRangeUpdated(TmfSelectionRangeUpdatedSignal signal) {
super.selectionRangeUpdated(signal);
if (signal != null && (signal.getSource() != this) && (getTrace() != null)) {
updateContent(this.getSelectionBeginTime(), this.getSelectionEndTime(), true);
}
}
/**
* Signal handler for handling of the window range signal. This time range
* is the visible zone of the view.
*
* @param signal
* The {@link TmfWindowRangeUpdatedSignal}
*/
@Override
@TmfSignalHandler
public void windowRangeUpdated(TmfWindowRangeUpdatedSignal signal) {
super.windowRangeUpdated(signal);
updateContent(this.getWindowStartTime(), this.getWindowEndTime(), false);
}
@Override
public void reset() {
super.reset();
clearContent();
}
}