/*******************************************************************************
* Copyright (c) 2012, 2016 Ericsson, 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:
* Patrick Tasse - Initial API and implementation
* François Rajotte - Filter implementation
* Geneviève Bastien - Add event links between entries
* Christian Mansky - Add check active / uncheck inactive buttons
*******************************************************************************/
package org.eclipse.tracecompass.tmf.ui.widgets.timegraph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeViewerListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeExpansionEvent;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridLayout;
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.Sash;
import org.eclipse.swt.widgets.Slider;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.ui.signal.TmfTimeViewAlignmentInfo;
import org.eclipse.tracecompass.tmf.ui.signal.TmfTimeViewAlignmentSignal;
import org.eclipse.tracecompass.tmf.ui.views.ITmfTimeAligned;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.dialogs.ITimeGraphEntryActiveProvider;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.dialogs.ShowFilterDialogAction;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ILinkEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.ITimeDataProvider;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.TimeGraphColorScheme;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.TimeGraphControl;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.TimeGraphMarkerAxis;
import com.google.common.collect.Iterables;
/**
* Time graph "combo" view (with the list/tree on the left and the gantt chart
* on the right)
*
* @author Patrick Tasse
* @deprecated Use {@link #getTimeGraphViewer()} instead.
*/
@Deprecated
public class TimeGraphCombo extends Composite {
// ------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------
/**
* Constant indicating that all levels of the time graph should be expanded
*/
public static final int ALL_LEVELS = AbstractTreeViewer.ALL_LEVELS;
private static final Object FILLER = new Object();
// ------------------------------------------------------------------------
// Fields
// ------------------------------------------------------------------------
/** The tree viewer */
private TreeViewer fTreeViewer;
/** The time viewer */
private @NonNull TimeGraphViewer fTimeGraphViewer;
/** The selection listener map */
private final Map<ITimeGraphSelectionListener, SelectionListenerWrapper> fSelectionListenerMap = new HashMap<>();
/** The map of viewer filters to viewer filter wrappers */
private final Map<@NonNull ViewerFilter, @NonNull ViewerFilter> fViewerFilterMap = new HashMap<>();
/**
* Flag to block the tree selection changed listener when triggered by the
* time graph combo
*/
private boolean fInhibitTreeSelection = false;
/** Number of filler rows used by the tree content provider */
private int fNumFillerRows;
/** Calculated item height for Linux workaround */
private int fLinuxItemHeight = 0;
/** The action that opens the filter dialog */
private ShowFilterDialogAction fShowFilterDialogAction;
/** Default weight of each part of the sash */
private static final int[] DEFAULT_WEIGHTS = { 1, 1 };
/** List of all expanded items whose parents are also expanded */
private List<TreeItem> fVisibleExpandedItems = null;
private Listener fSashDragListener;
private SashForm fSashForm;
private final boolean fScrollBarsInTreeWorkaround;
private Font fTreeFont;
// ------------------------------------------------------------------------
// Classes
// ------------------------------------------------------------------------
/**
* The TimeGraphViewerExtension is used to set appropriate values and to
* override methods that could be called directly by the user and that must
* be handled by the time graph combo.
*/
private class TimeGraphViewerExtension extends TimeGraphViewer {
private TimeGraphViewerExtension(Composite parent, int style, Tree tree) {
super(parent, style);
setItemHeight(TimeGraphCombo.this.getItemHeight(tree, true));
setHeaderHeight(tree.getHeaderHeight());
setBorderWidth(tree.getBorderWidth());
setNameWidthPref(0);
}
@Override
public ShowFilterDialogAction getShowFilterDialogAction() {
return TimeGraphCombo.this.getShowFilterDialogAction();
}
@Override
protected TimeGraphControl createTimeGraphControl(Composite composite, TimeGraphColorScheme colors) {
return new TimeGraphControl(composite, colors) {
@Override
public void verticalZoom(boolean zoomIn) {
TimeGraphCombo.this.verticalZoom(zoomIn);
}
@Override
public void resetVerticalZoom() {
TimeGraphCombo.this.resetVerticalZoom();
}
@Override
public void setElementPosition(ITimeGraphEntry entry, int y) {
/*
* Queue the update to make sure the time graph combo has
* finished updating the item heights.
*/
getDisplay().asyncExec(() -> {
if (isDisposed()) {
return;
}
super.setElementPosition(entry, y);
alignTreeItems(false);
});
}
};
}
private class TimeGraphMarkerAxisExtension extends TimeGraphMarkerAxis {
private int fMargin = 0;
public TimeGraphMarkerAxisExtension(Composite parent, @NonNull TimeGraphColorScheme colorScheme, @NonNull ITimeDataProvider timeProvider) {
super(parent, colorScheme, timeProvider);
}
@Override
public Point computeSize(int wHint, int hHint, boolean changed) {
Point size = super.computeSize(wHint, hHint, changed);
if (size.y > 0) {
size.y += fMargin;
}
return size;
}
@Override
public void redraw() {
super.redraw();
fTreeViewer.getControl().redraw();
}
@Override
protected void drawMarkerAxis(Rectangle bounds, int nameSpace, GC gc) {
super.drawMarkerAxis(bounds, nameSpace, gc);
}
private Rectangle getAxisBounds() {
Tree tree = fTreeViewer.getTree();
Rectangle axisBounds = getBounds();
Rectangle treeClientArea = tree.getClientArea();
if (axisBounds.isEmpty()) {
treeClientArea.y += treeClientArea.height;
treeClientArea.height = 0;
return treeClientArea;
}
Composite axisParent = getParent();
Point axisDisplayCoordinates = axisParent.toDisplay(axisBounds.x, axisBounds.y);
Point axisTreeCoordinates = tree.toControl(axisDisplayCoordinates);
int height = treeClientArea.y + treeClientArea.height - axisTreeCoordinates.y;
int margin = Math.max(0, axisBounds.height - height);
if (fMargin != margin) {
fMargin = margin;
getParent().layout();
redraw();
axisTreeCoordinates.y -= margin;
height += margin;
}
return new Rectangle(treeClientArea.x, axisTreeCoordinates.y, treeClientArea.width, height);
}
}
@Override
protected TimeGraphMarkerAxis createTimeGraphMarkerAxis(Composite parent, @NonNull TimeGraphColorScheme colorScheme, @NonNull ITimeDataProvider timeProvider) {
TimeGraphMarkerAxisExtension timeGraphMarkerAxis = new TimeGraphMarkerAxisExtension(parent, colorScheme, timeProvider);
Tree tree = fTreeViewer.getTree();
tree.addPaintListener(e -> {
/*
* Draw the marker axis over the tree. Only the name area will
* be drawn, since the time area has zero width.
*/
Rectangle bounds = timeGraphMarkerAxis.getAxisBounds();
e.gc.setBackground(timeGraphMarkerAxis.getBackground());
timeGraphMarkerAxis.drawMarkerAxis(bounds, bounds.width, e.gc);
});
tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(MouseEvent e) {
Rectangle bounds = timeGraphMarkerAxis.getAxisBounds();
if (bounds.contains(e.x, e.y)) {
timeGraphMarkerAxis.mouseDown(e, bounds, bounds.width);
}
}
});
tree.getHorizontalBar().addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
tree.redraw();
}
});
return timeGraphMarkerAxis;
}
}
/**
* The TreeContentProviderWrapper is used to insert filler items after the
* elements of the tree's real content provider.
*/
private class TreeContentProviderWrapper implements ITreeContentProvider {
private final ITreeContentProvider contentProvider;
public TreeContentProviderWrapper(ITreeContentProvider contentProvider) {
this.contentProvider = contentProvider;
}
@Override
public void dispose() {
contentProvider.dispose();
}
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
contentProvider.inputChanged(viewer, oldInput, newInput);
}
@Override
public Object[] getElements(Object inputElement) {
Object[] elements = contentProvider.getElements(inputElement);
// add filler elements to ensure alignment with time analysis viewer
Object[] oElements = Arrays.copyOf(elements, elements.length + fNumFillerRows, Object[].class);
for (int i = 0; i < fNumFillerRows; i++) {
oElements[elements.length + i] = FILLER;
}
return oElements;
}
@Override
public Object[] getChildren(Object parentElement) {
if (parentElement instanceof ITimeGraphEntry) {
return contentProvider.getChildren(parentElement);
}
return new Object[0];
}
@Override
public Object getParent(Object element) {
if (element instanceof ITimeGraphEntry) {
return contentProvider.getParent(element);
}
return null;
}
@Override
public boolean hasChildren(Object element) {
if (element instanceof ITimeGraphEntry) {
return contentProvider.hasChildren(element);
}
return false;
}
}
/**
* The TreeLabelProviderWrapper is used to intercept the filler items from
* the calls to the tree's real label provider.
*/
private static class TreeLabelProviderWrapper implements ITableLabelProvider {
private final ITableLabelProvider labelProvider;
public TreeLabelProviderWrapper(ITableLabelProvider labelProvider) {
this.labelProvider = labelProvider;
}
@Override
public void addListener(ILabelProviderListener listener) {
labelProvider.addListener(listener);
}
@Override
public void dispose() {
labelProvider.dispose();
}
@Override
public boolean isLabelProperty(Object element, String property) {
if (element instanceof ITimeGraphEntry) {
return labelProvider.isLabelProperty(element, property);
}
return false;
}
@Override
public void removeListener(ILabelProviderListener listener) {
labelProvider.removeListener(listener);
}
@Override
public Image getColumnImage(Object element, int columnIndex) {
if (element instanceof ITimeGraphEntry) {
return labelProvider.getColumnImage(element, columnIndex);
}
return null;
}
@Override
public String getColumnText(Object element, int columnIndex) {
if (element instanceof ITimeGraphEntry) {
return labelProvider.getColumnText(element, columnIndex);
}
return null;
}
}
/**
* The SelectionListenerWrapper is used to intercept the filler items from
* the time graph combo's real selection listener, and to prevent double
* notifications from being sent when selection changes in both tree and
* time graph at the same time.
*/
private class SelectionListenerWrapper implements ISelectionChangedListener, ITimeGraphSelectionListener {
private final ITimeGraphSelectionListener listener;
private ITimeGraphEntry selection = null;
public SelectionListenerWrapper(ITimeGraphSelectionListener listener) {
this.listener = listener;
}
@Override
public void selectionChanged(SelectionChangedEvent event) {
if (fInhibitTreeSelection) {
return;
}
Object element = ((IStructuredSelection) event.getSelection()).getFirstElement();
if (element instanceof ITimeGraphEntry) {
ITimeGraphEntry entry = (ITimeGraphEntry) element;
if (entry != selection) {
selection = entry;
listener.selectionChanged(new TimeGraphSelectionEvent(event.getSource(), selection));
}
}
}
@Override
public void selectionChanged(TimeGraphSelectionEvent event) {
ITimeGraphEntry entry = event.getSelection();
if (entry != selection) {
selection = entry;
listener.selectionChanged(new TimeGraphSelectionEvent(event.getSource(), selection));
}
}
}
/**
* The ViewerFilterWrapper is used to intercept the filler items from the
* time graph combo's real ViewerFilters. These filler items should always
* be visible.
*/
private static class ViewerFilterWrapper extends ViewerFilter {
private ViewerFilter fWrappedFilter;
ViewerFilterWrapper(ViewerFilter filter) {
super();
this.fWrappedFilter = filter;
}
@Override
public boolean select(Viewer viewer, Object parentElement, Object element) {
if (element instanceof ITimeGraphEntry) {
return fWrappedFilter.select(viewer, parentElement, element);
}
return true;
}
}
// ------------------------------------------------------------------------
// Constructors
// ------------------------------------------------------------------------
/**
* Constructs a new instance of this class given its parent and a style
* value describing its behavior and appearance.
*
* @param parent
* a widget which will be the parent of the new instance (cannot
* be null)
* @param style
* the style of widget to construct
*/
public TimeGraphCombo(Composite parent, int style) {
this(parent, style, DEFAULT_WEIGHTS);
}
/**
* Constructs a new instance of this class given its parent and a style
* value describing its behavior and appearance.
*
* @param parent
* a widget which will be the parent of the new instance (cannot
* be null)
* @param style
* the style of widget to construct
* @param weights
* The array (length 2) of relative weights of each side of the sash form
*/
public TimeGraphCombo(Composite parent, int style, int[] weights) {
super(parent, style);
setLayout(new FillLayout());
fSashForm = new SashForm(this, SWT.NONE);
/*
* In Windows, SWT.H_SCROLL | SWT.NO_SCROLL is not properly supported,
* both scroll bars are always created. See Tree.checkStyle: "Even when
* WS_HSCROLL or WS_VSCROLL is not specified, Windows creates trees and
* tables with scroll bars."
*/
fScrollBarsInTreeWorkaround = "win32".equals(SWT.getPlatform()); //$NON-NLS-1$
int scrollBarStyle = fScrollBarsInTreeWorkaround ? SWT.H_SCROLL : SWT.H_SCROLL | SWT.NO_SCROLL;
fTreeViewer = new TreeViewer(fSashForm, SWT.FULL_SELECTION | scrollBarStyle);
fTreeViewer.setAutoExpandLevel(AbstractTreeViewer.ALL_LEVELS);
final Tree tree = fTreeViewer.getTree();
tree.setHeaderVisible(true);
tree.setLinesVisible(true);
fTimeGraphViewer = new TimeGraphViewerExtension(fSashForm, SWT.NONE, tree);
fTimeGraphViewer.setColumns(new String[0]);
fTimeGraphViewer.setNameWidthPref(0);
fTimeGraphViewer.setWeights(new int[] { 0, 1 } );
if (fScrollBarsInTreeWorkaround) {
// Feature in Windows. The tree vertical bar reappears when
// the control is resized so we need to hide it again.
tree.addControlListener(new ControlAdapter() {
private int depth = 0;
@Override
public void controlResized(ControlEvent e) {
if (depth == 0) {
depth++;
tree.getVerticalBar().setEnabled(false);
// this can trigger controlResized recursively
tree.getVerticalBar().setVisible(false);
depth--;
}
}
});
}
// Bug in Linux. The tree header height is 0 in constructor,
// so we need to reset it later when the control is painted.
// This work around used to be done on control resized but the header
// height was not initialized on the initial resize on GTK3.
tree.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
int headerHeight = tree.getHeaderHeight();
if (headerHeight > 0) {
fTimeGraphViewer.setHeaderHeight(headerHeight);
tree.removePaintListener(this);
}
}
});
tree.addDisposeListener(e -> {
if (fTreeFont != null) {
fTreeFont.dispose();
}
});
// ensure synchronization of expanded items between tree and time graph
fTreeViewer.addTreeListener(new ITreeViewerListener() {
@Override
public void treeCollapsed(TreeExpansionEvent event) {
fTimeGraphViewer.setExpandedState((ITimeGraphEntry) event.getElement(), false);
// queue the alignment update because the tree items may only be
// actually collapsed after the listeners have been notified
fVisibleExpandedItems = null; // invalidate the cache
getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
alignTreeItems(true);
}});
}
@Override
public void treeExpanded(TreeExpansionEvent event) {
ITimeGraphEntry entry = (ITimeGraphEntry) event.getElement();
fTimeGraphViewer.setExpandedState(entry, true);
Set<Object> expandedElements = new HashSet<>(Arrays.asList(fTreeViewer.getExpandedElements()));
for (ITimeGraphEntry child : entry.getChildren()) {
if (child.hasChildren()) {
boolean expanded = expandedElements.contains(child);
fTimeGraphViewer.setExpandedState(child, expanded);
}
}
// queue the alignment update because the tree items may only be
// actually expanded after the listeners have been notified
fVisibleExpandedItems = null; // invalidate the cache
getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
alignTreeItems(true);
}});
}
});
// ensure synchronization of expanded items between tree and time graph
fTimeGraphViewer.addTreeListener(new ITimeGraphTreeListener() {
@Override
public void treeCollapsed(TimeGraphTreeExpansionEvent event) {
fTreeViewer.setExpandedState(event.getEntry(), false);
alignTreeItems(true);
}
@Override
public void treeExpanded(TimeGraphTreeExpansionEvent event) {
ITimeGraphEntry entry = event.getEntry();
fTreeViewer.setExpandedState(entry, true);
Set<Object> expandedElements = new HashSet<>(Arrays.asList(fTreeViewer.getExpandedElements()));
for (ITimeGraphEntry child : entry.getChildren()) {
if (child.hasChildren()) {
boolean expanded = expandedElements.contains(child);
fTimeGraphViewer.setExpandedState(child, expanded);
}
}
alignTreeItems(true);
}
});
// prevent mouse button from selecting a filler tree item
tree.addListener(SWT.MouseDown, event -> {
List<TreeItem> treeItems = getVisibleExpandedItems(tree, false);
if (treeItems.isEmpty()) {
event.doit = false;
fTreeViewer.setSelection(new StructuredSelection());
fTimeGraphViewer.setSelection(null, false);
return;
}
TreeItem lastTreeItem = treeItems.get(treeItems.size() - 1);
if (event.y >= lastTreeItem.getBounds().y + lastTreeItem.getBounds().height) {
event.doit = false;
// this prevents from scrolling up when selecting
// the partially visible tree item at the bottom
tree.select(treeItems.get(treeItems.size() - 1));
fTreeViewer.setSelection(new StructuredSelection());
fTimeGraphViewer.setSelection(null);
}
});
// prevent mouse wheel from scrolling down into filler tree items
tree.addListener(SWT.MouseWheel, event -> {
event.doit = false;
if (event.count == 0) {
return;
}
Slider scrollBar = fTimeGraphViewer.getVerticalBar();
fTimeGraphViewer.setTopIndex(scrollBar.getSelection() - event.count);
alignTreeItems(false);
});
// prevent key stroke from selecting a filler tree item
tree.addListener(SWT.KeyDown, event -> {
List<TreeItem> treeItems = getVisibleExpandedItems(tree, false);
if (treeItems.size() == 0) {
fTreeViewer.setSelection(new StructuredSelection());
event.doit = false;
return;
}
if (event.keyCode == SWT.ARROW_DOWN) {
int index = Math.min(fTimeGraphViewer.getSelectionIndex() + 1, treeItems.size() - 1);
fTimeGraphViewer.setSelection((ITimeGraphEntry) treeItems.get(index).getData(), true);
event.doit = false;
} else if (event.keyCode == SWT.PAGE_DOWN) {
int height = tree.getSize().y - tree.getHeaderHeight() - tree.getHorizontalBar().getSize().y;
int countPerPage = height / getItemHeight(tree, false);
int index = Math.min(fTimeGraphViewer.getSelectionIndex() + countPerPage - 1, treeItems.size() - 1);
fTimeGraphViewer.setSelection((ITimeGraphEntry) treeItems.get(index).getData());
event.doit = false;
} else if (event.keyCode == SWT.END) {
fTimeGraphViewer.setSelection((ITimeGraphEntry) treeItems.get(treeItems.size() - 1).getData());
event.doit = false;
} else if ((event.character == '+' || event.character == '=') && ((event.stateMask & SWT.CTRL) != 0)) {
fTimeGraphViewer.getTimeGraphControl().keyPressed(new KeyEvent(event));
return;
} else if (event.character == '-' && ((event.stateMask & SWT.CTRL) != 0)) {
fTimeGraphViewer.getTimeGraphControl().keyPressed(new KeyEvent(event));
return;
} else if (event.character == '0' && ((event.stateMask & SWT.CTRL) != 0)) {
fTimeGraphViewer.getTimeGraphControl().keyPressed(new KeyEvent(event));
return;
} else {
return;
}
if (fTimeGraphViewer.getSelectionIndex() >= 0) {
fTreeViewer.setSelection(new StructuredSelection(fTimeGraphViewer.getSelection()));
} else {
fTreeViewer.setSelection(new StructuredSelection());
}
alignTreeItems(false);
});
// ensure alignment of top item between tree and time graph
fTimeGraphViewer.getTimeGraphControl().addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
alignTreeItems(false);
}
});
// ensure synchronization of selected item between tree and time graph
fTreeViewer.addSelectionChangedListener(event -> {
if (fInhibitTreeSelection) {
return;
}
if (event.getSelection() instanceof IStructuredSelection) {
Object selection = ((IStructuredSelection) event.getSelection()).getFirstElement();
if (selection instanceof ITimeGraphEntry) {
fTimeGraphViewer.setSelection((ITimeGraphEntry) selection, true);
}
alignTreeItems(false);
}
});
// ensure synchronization of selected item between tree and time graph
fTimeGraphViewer.addSelectionListener(event -> {
ITimeGraphEntry entry = fTimeGraphViewer.getSelection();
setSelectionInTree(entry);
});
// ensure alignment of top item between tree and time graph
fTimeGraphViewer.getVerticalBar().addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
alignTreeItems(false);
}
});
// ensure alignment of top item between tree and time graph
fTimeGraphViewer.getTimeGraphControl().addMouseWheelListener(e -> {
if (e.count == 0) {
return;
}
alignTreeItems(false);
});
// ensure the tree has focus control when mouse is over it if the time graph had control
fTreeViewer.getControl().addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
if (fTimeGraphViewer.getTimeGraphControl().isFocusControl()) {
fTreeViewer.getControl().setFocus();
}
}
});
// ensure the time graph has focus control when mouse is over it if the tree had control
fTimeGraphViewer.getTimeGraphControl().addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
if (fTreeViewer.getControl().isFocusControl()) {
fTimeGraphViewer.getTimeGraphControl().setFocus();
}
}
});
fTimeGraphViewer.getTimeGraphScale().addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
if (fTreeViewer.getControl().isFocusControl()) {
fTimeGraphViewer.getTimeGraphControl().setFocus();
}
}
});
// The filler rows are required to ensure alignment when the tree does not have a
// visible horizontal scroll bar. The tree does not allow its top item to be set
// to a value that would cause blank space to be drawn at the bottom of the tree.
fNumFillerRows = Display.getDefault().getBounds().height / getItemHeight(tree, false);
fSashForm.setWeights(weights);
fTimeGraphViewer.getTimeGraphControl().addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
// Sashes in a SashForm are being created on layout so add the
// drag listener here
if (fSashDragListener == null) {
for (Control control : fSashForm.getChildren()) {
if (control instanceof Sash) {
fSashDragListener = new Listener() {
@Override
public void handleEvent(Event event) {
sendTimeViewAlignmentChanged();
}
};
control.removePaintListener(this);
control.addListener(SWT.Selection, fSashDragListener);
// There should be only one sash
break;
}
}
}
}
});
}
private void verticalZoom(boolean zoomIn) {
Tree tree = fTreeViewer.getTree();
FontData fontData = tree.getFont().getFontData()[0];
int height = fontData.getHeight() + (zoomIn ? 1 : -1);
if (height <= 0) {
return;
}
fontData.setHeight(height);
if (fTreeFont != null) {
fTreeFont.dispose();
}
fTreeFont = new Font(tree.getDisplay(), fontData);
tree.setFont(fTreeFont);
redraw();
update();
fTimeGraphViewer.setHeaderHeight(tree.getHeaderHeight());
fTimeGraphViewer.setItemHeight(getItemHeight(tree, true));
alignTreeItems(false);
}
private void resetVerticalZoom() {
Tree tree = fTreeViewer.getTree();
if (fTreeFont != null) {
fTreeFont.dispose();
fTreeFont = null;
}
tree.setFont(null);
redraw();
update();
fTimeGraphViewer.setHeaderHeight(tree.getHeaderHeight());
fTimeGraphViewer.setItemHeight(getItemHeight(tree, true));
alignTreeItems(false);
}
private void sendTimeViewAlignmentChanged() {
TmfSignalManager.dispatchSignal(new TmfTimeViewAlignmentSignal(fSashForm, getTimeViewAlignmentInfo()));
}
// ------------------------------------------------------------------------
// Accessors
// ------------------------------------------------------------------------
/**
* Returns this time graph combo's tree viewer.
*
* @return the tree viewer
*/
public TreeViewer getTreeViewer() {
return fTreeViewer;
}
/**
* Returns this time graph combo's time graph viewer.
*
* @return the time graph viewer
*/
public @NonNull TimeGraphViewer getTimeGraphViewer() {
return fTimeGraphViewer;
}
/**
* Get the show filter dialog action.
*
* @return The Action object
* @since 1.2
*/
public ShowFilterDialogAction getShowFilterDialogAction() {
if (fShowFilterDialogAction == null) {
fShowFilterDialogAction = new ShowFilterDialogAction(fTimeGraphViewer) {
@Override
protected void addFilter(ViewerFilter filter) {
/* add filter to the combo instead of the viewer */
TimeGraphCombo.this.addFilter(filter);
}
@Override
protected void removeFilter(ViewerFilter filter) {
/* remove filter from the combo instead of the viewer */
TimeGraphCombo.this.removeFilter(filter);
}
@Override
protected void refresh() {
/* refresh the combo instead of the viewer */
TimeGraphCombo.this.refresh();
}
};
}
return fShowFilterDialogAction;
}
// ------------------------------------------------------------------------
// Control
// ------------------------------------------------------------------------
@Override
public void redraw() {
fTimeGraphViewer.getControl().redraw();
super.redraw();
}
@Override
public void update() {
fTimeGraphViewer.getControl().update();
super.update();
}
// ------------------------------------------------------------------------
// Operations
// ------------------------------------------------------------------------
/**
* Sets the tree content provider used by this time graph combo.
*
* @param contentProvider
* the tree content provider
*/
public void setTreeContentProvider(ITreeContentProvider contentProvider) {
fTreeViewer.setContentProvider(new TreeContentProviderWrapper(contentProvider));
}
/**
* Sets the tree label provider used by this time graph combo.
*
* @param labelProvider
* the tree label provider
*/
public void setTreeLabelProvider(ITableLabelProvider labelProvider) {
fTreeViewer.setLabelProvider(new TreeLabelProviderWrapper(labelProvider));
}
/**
* Sets the tree content provider used by the filter dialog
*
* @param contentProvider
* the tree content provider
*/
public void setFilterContentProvider(ITreeContentProvider contentProvider) {
getShowFilterDialogAction().getFilterDialog().setContentProvider(contentProvider);
}
/**
* Sets the tree label provider used by the filter dialog
*
* @param labelProvider
* the tree label provider
*/
public void setFilterLabelProvider(ITableLabelProvider labelProvider) {
getShowFilterDialogAction().getFilterDialog().setLabelProvider(labelProvider);
}
/**
* Adds a "check active" button used by the filter dialog
*
* @param activeProvider
* Additional button info specific to a certain view.
* @since 1.0
*/
public void addTimeGraphFilterCheckActiveButton(ITimeGraphEntryActiveProvider activeProvider) {
getShowFilterDialogAction().getFilterDialog().addTimeGraphFilterCheckActiveButton(activeProvider);
}
/**
* Adds an "uncheck inactive" button used by the filter dialog
*
* @param inactiveProvider
* Additional button info specific to a certain view.
* @since 1.0
*/
public void addTimeGraphFilterUncheckInactiveButton(ITimeGraphEntryActiveProvider inactiveProvider) {
getShowFilterDialogAction().getFilterDialog().addTimeGraphFilterUncheckInactiveButton(inactiveProvider);
}
/**
* Sets the tree columns for this time graph combo.
*
* @param columnNames
* the tree column names
*/
public void setTreeColumns(String[] columnNames) {
final Tree tree = fTreeViewer.getTree();
for (String columnName : columnNames) {
TreeColumn column = new TreeColumn(tree, SWT.LEFT);
column.setMoveable(true);
column.setText(columnName);
column.pack();
}
}
/**
* Sets the tree columns for this time graph combo's filter dialog.
*
* @param columnNames
* the tree column names
*/
public void setFilterColumns(String[] columnNames) {
getShowFilterDialogAction().getFilterDialog().setColumnNames(columnNames);
}
/**
* Sets the time graph content provider used by this time graph combo.
*
* @param timeGraphContentProvider
* the time graph content provider
*/
public void setTimeGraphContentProvider(ITimeGraphContentProvider timeGraphContentProvider) {
fTimeGraphViewer.setTimeGraphContentProvider(timeGraphContentProvider);
}
/**
* Sets the time graph presentation provider used by this time graph combo.
*
* @param timeGraphProvider
* the time graph provider
*/
public void setTimeGraphProvider(ITimeGraphPresentationProvider timeGraphProvider) {
fTimeGraphViewer.setTimeGraphProvider(timeGraphProvider);
}
/**
* Sets or clears the input for this time graph combo.
*
* @param input
* the input of this time graph combo, or <code>null</code> if
* none
*/
public void setInput(Object input) {
fInhibitTreeSelection = true;
fTreeViewer.setInput(input);
for (SelectionListenerWrapper listenerWrapper : fSelectionListenerMap.values()) {
listenerWrapper.selection = null;
}
fInhibitTreeSelection = false;
if (fScrollBarsInTreeWorkaround) {
fTreeViewer.getTree().getVerticalBar().setEnabled(false);
fTreeViewer.getTree().getVerticalBar().setVisible(false);
}
fTimeGraphViewer.setInput(input);
fTimeGraphViewer.setItemHeight(getItemHeight(fTreeViewer.getTree(), false));
// queue the alignment update because in Linux the item bounds are not
// set properly until the tree has been painted at least once
fVisibleExpandedItems = null; // invalidate the cache
getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (isDisposed()) {
return;
}
alignTreeItems(true);
}
});
}
/**
* Gets the input for this time graph combo.
*
* @return The input of this time graph combo, or <code>null</code> if none
*/
public Object getInput() {
return fTreeViewer.getInput();
}
/**
* Sets or clears the list of links to display on this combo
*
* @param links
* the links to display in this time graph combo
*/
public void setLinks(List<ILinkEvent> links) {
fTimeGraphViewer.setLinks(links);
}
/**
* @param filter
* The filter object to be attached to the view
*/
public void addFilter(@NonNull ViewerFilter filter) {
fInhibitTreeSelection = true;
ViewerFilter wrapper = new ViewerFilterWrapper(filter);
fTreeViewer.addFilter(wrapper);
fTimeGraphViewer.addFilter(filter);
fViewerFilterMap.put(filter, wrapper);
alignTreeItems(true);
fInhibitTreeSelection = false;
}
/**
* @param filter
* The filter object to be removed from the view
*/
public void removeFilter(@NonNull ViewerFilter filter) {
fInhibitTreeSelection = true;
ViewerFilter wrapper = fViewerFilterMap.get(filter);
fTreeViewer.removeFilter(wrapper);
fTimeGraphViewer.removeFilter(filter);
fViewerFilterMap.remove(filter);
alignTreeItems(true);
fInhibitTreeSelection = false;
}
/**
* Returns this viewer's filters.
*
* @return an array of viewer filters
* @since 1.2
*/
public @NonNull ViewerFilter[] getFilters() {
return fTimeGraphViewer.getFilters();
}
/**
* Sets the filters, replacing any previous filters, and triggers
* refiltering of the elements.
*
* @param filters
* an array of viewer filters, or null
* @since 1.2
*/
public void setFilters(@NonNull ViewerFilter[] filters) {
fInhibitTreeSelection = true;
fViewerFilterMap.clear();
if (filters == null) {
fTreeViewer.resetFilters();
} else {
for (ViewerFilter filter : filters) {
ViewerFilter wrapper = new ViewerFilterWrapper(filter);
fViewerFilterMap.put(filter, wrapper);
}
ViewerFilter[] wrappers = Iterables.toArray(fViewerFilterMap.values(), ViewerFilter.class);
fTreeViewer.setFilters(wrappers);
}
fTimeGraphViewer.setFilters(filters);
alignTreeItems(true);
fInhibitTreeSelection = false;
}
/**
* Refreshes this time graph completely with information freshly obtained
* from its model.
*/
public void refresh() {
fInhibitTreeSelection = true;
Tree tree = fTreeViewer.getTree();
try {
tree.setRedraw(false);
fTreeViewer.refresh();
} finally {
tree.setRedraw(true);
}
fTimeGraphViewer.refresh();
alignTreeItems(true);
fInhibitTreeSelection = false;
}
/**
* Adds a listener for selection changes in this time graph combo.
*
* @param listener
* a selection listener
*/
public void addSelectionListener(ITimeGraphSelectionListener listener) {
SelectionListenerWrapper listenerWrapper = new SelectionListenerWrapper(listener);
fTreeViewer.addSelectionChangedListener(listenerWrapper);
fSelectionListenerMap.put(listener, listenerWrapper);
fTimeGraphViewer.addSelectionListener(listenerWrapper);
}
/**
* Removes the given selection listener from this time graph combo.
*
* @param listener
* a selection changed listener
*/
public void removeSelectionListener(ITimeGraphSelectionListener listener) {
SelectionListenerWrapper listenerWrapper = fSelectionListenerMap.remove(listener);
fTreeViewer.removeSelectionChangedListener(listenerWrapper);
fTimeGraphViewer.removeSelectionListener(listenerWrapper);
}
/**
* Sets the current selection for this time graph combo.
*
* @param selection
* the new selection
*/
public void setSelection(ITimeGraphEntry selection) {
fTimeGraphViewer.setSelection(selection, true);
setSelectionInTree(selection);
}
/**
* Sets the current selection for this time graph combo and reveal it if
* needed.
*
* @param selection
* The new selection
* @since 2.0
*/
public void selectAndReveal(@NonNull ITimeGraphEntry selection) {
fTimeGraphViewer.selectAndReveal(selection);
setSelectionInTree(selection);
}
/**
* Select the entry in the tree structure
*
* @param selection
* The new selection
*/
private void setSelectionInTree(ITimeGraphEntry selection) {
fInhibitTreeSelection = true; // block the tree selection changed
// listener
if (selection != null) {
StructuredSelection structuredSelection = new StructuredSelection(selection);
fTreeViewer.setSelection(structuredSelection);
} else {
fTreeViewer.setSelection(new StructuredSelection());
}
fInhibitTreeSelection = false;
alignTreeItems(false);
}
/**
* Sets the auto-expand level to be used for new entries discovered when
* calling {@link #setInput(Object)} or {@link #refresh()}. The value 0
* means that there is no auto-expand; 1 means that top-level entries are
* expanded, but not their children; 2 means that top-level entries are
* expanded, and their children, but not grand-children; and so on.
* <p>
* The value {@link #ALL_LEVELS} means that all subtrees should be expanded.
* </p>
*
* @param level
* non-negative level, or <code>ALL_LEVELS</code> to expand all
* levels of the tree
*/
public void setAutoExpandLevel(int level) {
fTimeGraphViewer.setAutoExpandLevel(level);
if (level <= 0) {
fTreeViewer.setAutoExpandLevel(level);
} else {
fTreeViewer.setAutoExpandLevel(level + 1);
}
}
/**
* Returns the auto-expand level.
*
* @return non-negative level, or <code>ALL_LEVELS</code> if all levels of
* the tree are expanded automatically
* @see #setAutoExpandLevel
*/
public int getAutoExpandLevel() {
return fTimeGraphViewer.getAutoExpandLevel();
}
/**
* Get the expanded state of an entry.
*
* @param entry
* The entry
* @return true if the entry is expanded, false if collapsed
* @since 2.0
*/
public boolean getExpandedState(ITimeGraphEntry entry) {
return fTimeGraphViewer.getExpandedState(entry);
}
/**
* Set the expanded state of an entry
*
* @param entry
* The entry to expand/collapse
* @param expanded
* True for expanded, false for collapsed
*/
public void setExpandedState(ITimeGraphEntry entry, boolean expanded) {
fTimeGraphViewer.setExpandedState(entry, expanded);
fTreeViewer.setExpandedState(entry, expanded);
alignTreeItems(true);
}
/**
* Collapses all nodes of the viewer's tree, starting with the root.
*/
public void collapseAll() {
fTimeGraphViewer.collapseAll();
fTreeViewer.collapseAll();
alignTreeItems(true);
}
/**
* Expands all nodes of the viewer's tree, starting with the root.
*/
public void expandAll() {
fTimeGraphViewer.expandAll();
fTreeViewer.expandAll();
alignTreeItems(true);
}
// ------------------------------------------------------------------------
// Internal
// ------------------------------------------------------------------------
private List<TreeItem> getVisibleExpandedItems(Tree tree, boolean refresh) {
if (fVisibleExpandedItems == null || refresh) {
List<TreeItem> visibleExpandedItems = new ArrayList<>();
addVisibleExpandedItems(visibleExpandedItems, tree.getItems());
fVisibleExpandedItems = visibleExpandedItems;
}
return fVisibleExpandedItems;
}
private void addVisibleExpandedItems(List<TreeItem> visibleExpandedItems, TreeItem[] items) {
for (TreeItem item : items) {
Object data = item.getData();
if (data == FILLER) {
break;
}
visibleExpandedItems.add(item);
boolean expandedState = fTimeGraphViewer.getExpandedState((ITimeGraphEntry) data);
if (item.getExpanded() != expandedState) {
/* synchronize the expanded state of both viewers */
fTreeViewer.setExpandedState(data, expandedState);
}
if (expandedState) {
addVisibleExpandedItems(visibleExpandedItems, item.getItems());
}
}
}
private int getItemHeight(final Tree tree, boolean force) {
/*
* Bug in Linux. The method getItemHeight doesn't always return the
* correct value.
*/
if (fLinuxItemHeight >= 0 && System.getProperty("os.name").contains("Linux")) { //$NON-NLS-1$ //$NON-NLS-2$
if (fLinuxItemHeight != 0 && !force) {
return fLinuxItemHeight;
}
if (getVisibleExpandedItems(tree, true).size() > 1) {
PaintListener paintListener = new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
// get the treeItems here to have all items
List<TreeItem> treeItems = getVisibleExpandedItems(tree, true);
if (treeItems.size() < 2) {
return;
}
final TreeItem treeItem0 = treeItems.get(0);
final TreeItem treeItem1 = treeItems.get(1);
tree.removePaintListener(this);
int y0 = treeItem0.getBounds().y;
int y1 = treeItem1.getBounds().y;
int itemHeight = y1 - y0;
if (itemHeight > 0) {
fLinuxItemHeight = itemHeight;
fTimeGraphViewer.setItemHeight(itemHeight);
}
}
};
tree.addPaintListener(paintListener);
}
} else {
fLinuxItemHeight = -1; // Not Linux, don't perform os.name check
// anymore
}
return tree.getItemHeight();
}
private void alignTreeItems(boolean refreshExpandedItems) {
// align the tree top item with the time graph top item
Tree tree = fTreeViewer.getTree();
List<TreeItem> treeItems = getVisibleExpandedItems(tree, refreshExpandedItems);
int topIndex = fTimeGraphViewer.getTopIndex();
if (topIndex >= treeItems.size()) {
return;
}
TreeItem item = treeItems.get(topIndex);
tree.setTopItem(item);
/*
* In GTK3, the bounds of the tree items are only sure to be correct
* after the tree has been painted.
*/
tree.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
tree.removePaintListener(this);
doAlignTreeItems();
redraw();
}
});
/* Make sure the paint event is triggered. */
tree.redraw();
}
private void doAlignTreeItems() {
Tree tree = fTreeViewer.getTree();
List<TreeItem> treeItems = getVisibleExpandedItems(tree, false);
int topIndex = fTimeGraphViewer.getTopIndex();
if (topIndex >= treeItems.size()) {
return;
}
TreeItem item = treeItems.get(topIndex);
// get the first filler item so we can calculate the last item's height
TreeItem fillerItem = null;
for (TreeItem treeItem : fTreeViewer.getTree().getItems()) {
if (treeItem.getData() == FILLER) {
fillerItem = treeItem;
break;
}
}
// ensure the time graph item heights are equal to the tree item heights
int treeHeight = fTreeViewer.getTree().getBounds().height;
int index = topIndex;
Rectangle bounds = item.getBounds();
while (index < treeItems.size()) {
if (bounds.y > treeHeight) {
break;
}
TreeItem nextItem = (index + 1 == treeItems.size()) ? fillerItem : treeItems.get(index + 1);
Rectangle nextBounds = alignTreeItem(item, bounds, nextItem);
index++;
item = nextItem;
bounds = nextBounds;
}
/*
* When an item's height in the time graph changes, it is possible that
* the time graph readjusts its top index to fill empty space at the
* bottom of the viewer. Calling method setTopIndex() triggers this
* adjustment, if needed. In that case, we need to make sure that the
* newly visible items at the top of the viewer are also aligned.
*/
fTimeGraphViewer.setTopIndex(topIndex);
if (fTimeGraphViewer.getTopIndex() != topIndex) {
alignTreeItems(false);
}
}
private Rectangle alignTreeItem(TreeItem item, Rectangle bounds, TreeItem nextItem) {
/*
* Bug in Linux. The method getBounds doesn't always return the correct
* height. Use the difference of y position between items to calculate
* the height.
*/
Rectangle nextBounds = nextItem.getBounds();
Integer itemHeight = nextBounds.y - bounds.y;
if (itemHeight > 0) {
ITimeGraphEntry entry = (ITimeGraphEntry) item.getData();
fTimeGraphViewer.getTimeGraphControl().setItemHeight(entry, itemHeight);
}
return nextBounds;
}
/**
* Return the time alignment information
*
* @return the time alignment information
*
* @see ITmfTimeAligned
*
* @since 1.0
*/
public TmfTimeViewAlignmentInfo getTimeViewAlignmentInfo() {
Point location = fSashForm.toDisplay(0, 0);
int timeAxisOffset = fTreeViewer.getControl().getSize().x + fSashForm.getSashWidth();
return new TmfTimeViewAlignmentInfo(fSashForm.getShell(), location, timeAxisOffset);
}
/**
* Return the available width for the time-axis.
*
* @see ITmfTimeAligned
*
* @param requestedOffset
* the requested offset
* @return the available width for the time-axis
*
* @since 1.0
*/
public int getAvailableWidth(int requestedOffset) {
int vBarWidth = ((fTimeGraphViewer.getVerticalBar() != null) && (fTimeGraphViewer.getVerticalBar().isVisible())) ? fTimeGraphViewer.getVerticalBar().getSize().x : 0;
int totalWidth = fSashForm.getBounds().width;
return Math.min(totalWidth, Math.max(0, totalWidth - requestedOffset - vBarWidth));
}
/**
* Perform the alignment operation.
*
* @param offset
* the alignment offset
* @param width
* the alignment width
*
* @see ITmfTimeAligned
*
* @since 1.0
*/
public void performAlign(int offset, int width) {
int total = fSashForm.getBounds().width;
int timeAxisOffset = Math.min(offset, total);
int width1 = Math.max(0, timeAxisOffset - fSashForm.getSashWidth());
int width2 = total - timeAxisOffset;
if (width1 >= 0 && width2 > 0 || width1 > 0 && width2 >= 0) {
fSashForm.setWeights(new int[] { width1, width2 });
fSashForm.layout();
}
Composite composite = fTimeGraphViewer.getTimeAlignedComposite();
GridLayout layout = (GridLayout) composite.getLayout();
int timeBasedControlsWidth = composite.getSize().x;
int marginSize = timeBasedControlsWidth - width;
layout.marginRight = Math.max(0, marginSize);
composite.layout();
}
}