blob: bcea00e5c5c2807d53e04e7a35a9cb5f186eef84 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Ericsson
*
* 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
*
* Contributors:
* Patrick Tasse - Initial API and implementation
*******************************************************************************/
package org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.graphics.Color;
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.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.IMarkerEvent;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
/**
* A control that shows marker labels on a time axis.
*
* @since 2.0
*/
public class TimeGraphMarkerAxis extends TimeGraphBaseControl {
private static final Image COLLAPSED = Activator.getDefault().getImageFromPath("icons/ovr16/collapsed_ovr.gif"); //$NON-NLS-1$
private static final Image EXPANDED = Activator.getDefault().getImageFromPath("icons/ovr16/expanded_ovr.gif"); //$NON-NLS-1$
private static final Image HIDE = Activator.getDefault().getImageFromPath("icons/etool16/hide.gif"); //$NON-NLS-1$
private static final int HIDE_BORDER = 4; // transparent border of the hide icon
private static final int HEIGHT;
static {
GC gc = new GC(Display.getDefault());
HEIGHT = gc.getFontMetrics().getHeight() + 1;
gc.dispose();
}
private static final int TOP_MARGIN = 1;
private static final int MAX_LABEL_LENGTH = 256;
private static final int TEXT_MARGIN = 2;
private static final int MAX_GAP = 5;
private static final int X_LIMIT = Integer.MAX_VALUE / 256;
private @NonNull ITimeDataProvider fTimeProvider;
private final Set<IMarkerAxisListener> fListeners = new LinkedHashSet<>();
private Multimap<String, IMarkerEvent> fMarkers = LinkedHashMultimap.create();
private @NonNull List<String> fCategories = Collections.emptyList();
private boolean fCollapsed = false;
/**
* Contructor
*
* @param parent
* The parent composite object
* @param colorScheme
* The color scheme to use
* @param timeProvider
* The time data provider
*/
public TimeGraphMarkerAxis(Composite parent, @NonNull TimeGraphColorScheme colorScheme, @NonNull ITimeDataProvider timeProvider) {
super(parent, colorScheme, SWT.NO_BACKGROUND | SWT.NO_FOCUS | SWT.DOUBLE_BUFFERED);
fTimeProvider = timeProvider;
addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(MouseEvent e) {
Point size = getSize();
Rectangle bounds = new Rectangle(0, 0, size.x, size.y);
TimeGraphMarkerAxis.this.mouseDown(e, bounds, fTimeProvider.getNameSpace());
}
});
}
@Override
public Point computeSize(int wHint, int hHint, boolean changed) {
int height = 0;
if (!fMarkers.isEmpty() && fTimeProvider.getTime0() != fTimeProvider.getTime1()) {
if (fCollapsed) {
height = COLLAPSED.getBounds().height;
} else {
height = TOP_MARGIN + fMarkers.keySet().size() * HEIGHT;
}
}
return super.computeSize(wHint, height, changed);
}
/**
* Add a marker axis listener.
*
* @param listener
* the listener
*/
public void addMarkerAxisListener(IMarkerAxisListener listener) {
fListeners.add(listener);
}
/**
* Remove a marker axis listener.
*
* @param listener
* the listener
*/
public void removeMarkerAxisListener(IMarkerAxisListener listener) {
fListeners.remove(listener);
}
/**
* Set the time provider
*
* @param timeProvider
* The provider to use
*/
public void setTimeProvider(@NonNull ITimeDataProvider timeProvider) {
fTimeProvider = timeProvider;
}
/**
* Set the list of marker categories.
*
* @param categories
* The list of marker categories, or null
*/
public void setMarkerCategories(List<String> categories) {
if (categories == null) {
fCategories = Collections.emptyList();
} else {
fCategories = categories;
}
}
/**
* Handle a mouseDown event.
*
* @param e
* the mouse event
* @param bounds
* the bounds of the marker axis in the mouse event's coordinates
* @param nameSpace
* the width of the marker name area
*/
public void mouseDown(MouseEvent e, Rectangle bounds, int nameSpace) {
if (bounds.isEmpty()) {
return;
}
if (fCollapsed || (e.x < bounds.x + Math.min(nameSpace, EXPANDED.getBounds().width))) {
fCollapsed = !fCollapsed;
getParent().layout();
redraw();
return;
}
if (e.x < bounds.x + nameSpace) {
String category = getHiddenCategoryForEvent(e, bounds);
if (category != null) {
for (IMarkerAxisListener listener : fListeners) {
listener.setMarkerCategoryVisible(category, false);
}
}
return;
}
IMarkerEvent marker = getMarkerForEvent(e);
if (marker != null) {
if ((e.stateMask & SWT.MODIFIER_MASK) == SWT.SHIFT) {
/* Extend current selection */
long selectionBegin = fTimeProvider.getSelectionBegin();
long selectionEnd = fTimeProvider.getSelectionEnd();
long markerBegin = marker.getTime();
long markerEnd = marker.getTime() + marker.getDuration();
/* If marker end is outside selection, extend closest boundary */
if (((markerEnd - selectionBegin) > 0) == ((markerEnd - selectionEnd) > 0)) {
if (Math.abs(markerEnd - selectionBegin) < Math.abs(markerEnd - selectionEnd)) {
selectionBegin = markerEnd;
} else {
selectionEnd = markerEnd;
}
}
/* If marker begin is outside selection, extend closest boundary */
if (((markerBegin - selectionBegin) > 0) == ((markerBegin - selectionEnd) > 0)) {
if (Math.abs(markerBegin - selectionBegin) < Math.abs(markerBegin - selectionEnd)) {
selectionBegin = markerBegin;
} else {
selectionEnd = markerBegin;
}
}
fTimeProvider.setSelectionRangeNotify(selectionBegin, selectionEnd, false);
} else {
/* Replace current selection */
fTimeProvider.setSelectionRangeNotify(marker.getTime(), marker.getTime() + marker.getDuration(), false);
}
}
}
/**
* Set the markers list.
*
* @param markers
* The markers list
*/
public void setMarkers(List<IMarkerEvent> markers) {
Multimap<String, IMarkerEvent> map = LinkedHashMultimap.create();
for (IMarkerEvent marker : markers) {
if (marker.getEntry() == null) {
map.put(marker.getCategory(), marker);
}
}
Display.getDefault().asyncExec(() -> {
if (isDisposed()) {
return;
}
fMarkers = map;
getParent().layout();
redraw();
});
}
@Override
void paint(Rectangle bounds, PaintEvent e) {
drawMarkerAxis(bounds, fTimeProvider.getNameSpace(), e.gc);
}
/**
* Draw the marker axis
*
* @param bounds
* the bounds of the marker axis
* @param nameSpace
* the width of the marker name area
* @param gc
* the GC instance
*/
protected void drawMarkerAxis(Rectangle bounds, int nameSpace, GC gc) {
if (bounds.isEmpty()) {
return;
}
// draw background
gc.fillRectangle(bounds);
if (!fCollapsed) {
Rectangle rect = new Rectangle(bounds.x, bounds.y + TOP_MARGIN, bounds.width, HEIGHT);
for (String category : getVisibleCategories()) {
rect.x = bounds.x;
rect.width = nameSpace;
drawMarkerCategory(category, rect, gc);
rect.x = nameSpace;
rect.width = bounds.width - nameSpace;
drawMarkerLabels(category, rect, gc);
rect.y += HEIGHT;
}
}
Rectangle rect = new Rectangle(bounds.x, bounds.y, nameSpace, bounds.height);
gc.setClipping(rect);
drawToolbar(rect, nameSpace, gc);
}
/**
* Draw the marker category
*
* @param category
* the category
* @param rect
* the bounds of the marker name area
* @param gc
* the GC instance
*/
protected void drawMarkerCategory(String category, Rectangle rect, GC gc) {
if (rect.isEmpty()) {
return;
}
// draw marker category
gc.setForeground(getColorScheme().getColor(TimeGraphColorScheme.TOOL_FOREGROUND));
gc.setClipping(rect);
int width = gc.textExtent(category).x + TEXT_MARGIN;
int x = rect.x + EXPANDED.getBounds().width + HIDE.getBounds().width;
gc.drawText(category, Math.max(x, rect.x + rect.width - width), rect.y, true);
}
/**
* Draw the marker labels for the specified category
*
* @param category
* the category
* @param rect
* the bounds of the marker time area
* @param gc
* the GC instance
*/
protected void drawMarkerLabels(String category, Rectangle rect, GC gc) {
if (rect.isEmpty()) {
return;
}
long time0 = fTimeProvider.getTime0();
long time1 = fTimeProvider.getTime1();
if (time0 == time1) {
return;
}
int timeSpace = fTimeProvider.getTimeSpace();
double pixelsPerNanoSec = (timeSpace <= RIGHT_MARGIN) ? 0 :
(double) (timeSpace - RIGHT_MARGIN) / (time1 - time0);
gc.setClipping(rect);
for (IMarkerEvent markerEvent : fMarkers.get(category)) {
Color color = getColorScheme().getColor(markerEvent.getColor());
gc.setForeground(color);
gc.setBackground(color);
int x1 = getXForTime(rect, time0, pixelsPerNanoSec, markerEvent.getTime());
if (x1 > rect.x + rect.width) {
return;
}
if (markerEvent.getEntry() != null) {
continue;
}
int x2 = getXForTime(rect, time0, pixelsPerNanoSec, markerEvent.getTime() + markerEvent.getDuration()) - 1;
String label = getTrimmedLabel(markerEvent);
if (label != null) {
int width = gc.textExtent(label).x + TEXT_MARGIN;
if (x1 < rect.x && x1 + width < x2) {
int gap = Math.min(rect.x - x1, MAX_GAP);
x1 = Math.min(rect.x + gap, x2 - width);
if (x1 > rect.x) {
int y = rect.y + rect.height / 2;
gc.drawLine(rect.x, y, x1, y);
}
}
gc.fillRectangle(x1, rect.y, width, rect.height - 1);
gc.drawRectangle(x1, rect.y, width, rect.height - 1);
if (x2 > x1 + width) {
int y = rect.y + rect.height / 2;
gc.drawLine(x1 + width, y, x2, y);
}
gc.setForeground(Utils.getDistinctColor(color.getRGB()));
Utils.drawText(gc, label, x1 + TEXT_MARGIN, rect.y, true);
} else {
int y = rect.y + rect.height / 2;
gc.drawLine(x1, y, x2, y);
}
}
}
/**
* Draw the toolbar
*
* @param bounds
* the bounds of the marker axis
* @param nameSpace
* the width of the marker name area
* @param gc
* the GC instance
*/
protected void drawToolbar(Rectangle bounds, int nameSpace, GC gc) {
if (bounds.isEmpty()) {
return;
}
if (fCollapsed) {
gc.drawImage(COLLAPSED, bounds.x, bounds.y);
} else {
gc.drawImage(EXPANDED, bounds.x, bounds.y);
int x = bounds.x + EXPANDED.getBounds().width;
for (int i = 0; i < fMarkers.keySet().size(); i++) {
int y = bounds.y + TOP_MARGIN + i * HEIGHT;
gc.drawImage(HIDE, x, y);
}
}
}
private static String getTrimmedLabel(IMarkerEvent marker) {
String label = marker.getLabel();
if (label == null) {
return null;
}
return label.substring(0, Math.min(label.indexOf(SWT.LF) != -1 ? label.indexOf(SWT.LF) : label.length(), MAX_LABEL_LENGTH));
}
private static int getXForTime(Rectangle rect, long time0, double pixelsPerNanoSec, long time) {
int x = rect.x + (int) (Math.floor((time - time0) * pixelsPerNanoSec));
return Math.min(Math.max(x, -X_LIMIT), X_LIMIT);
}
private IMarkerEvent getMarkerForEvent(MouseEvent event) {
long time0 = fTimeProvider.getTime0();
long time1 = fTimeProvider.getTime1();
if (time0 == time1) {
return null;
}
int timeSpace = fTimeProvider.getTimeSpace();
double pixelsPerNanoSec = (timeSpace <= RIGHT_MARGIN) ? 0 :
(double) (timeSpace - RIGHT_MARGIN) / (time1 - time0);
int categoryIndex = Math.max((event.y - TOP_MARGIN) / HEIGHT, 0);
if (categoryIndex >= getVisibleCategories().size()) {
return null;
}
String category = getVisibleCategories().get(categoryIndex);
IMarkerEvent marker = null;
GC gc = new GC(Display.getDefault());
Rectangle rect = getBounds();
rect.x += fTimeProvider.getNameSpace();
rect.width -= fTimeProvider.getNameSpace();
for (IMarkerEvent markerEvent : fMarkers.get(category)) {
String label = getTrimmedLabel(markerEvent);
if (markerEvent.getEntry() == null) {
int x1 = getXForTime(rect, time0, pixelsPerNanoSec, markerEvent.getTime());
if (x1 <= event.x) {
if (label != null) {
int width = gc.textExtent(label).x + TEXT_MARGIN;
if (event.x <= x1 + width) {
marker = markerEvent;
continue;
}
}
int x2 = getXForTime(rect, time0, pixelsPerNanoSec, markerEvent.getTime() + markerEvent.getDuration()) - 1;
if (event.x <= x2) {
marker = markerEvent;
}
} else {
break;
}
}
}
gc.dispose();
return marker;
}
private String getHiddenCategoryForEvent(MouseEvent e, Rectangle bounds) {
List<String> categories = getVisibleCategories();
Rectangle rect = HIDE.getBounds();
rect.x += bounds.x + EXPANDED.getBounds().width + HIDE_BORDER;
rect.y += bounds.y + TOP_MARGIN + HIDE_BORDER;
rect.width -= 2 * HIDE_BORDER;
rect.height -= 2 * HIDE_BORDER;
for (int i = 0; i < categories.size(); i++) {
if (rect.contains(e.x, e.y)) {
return categories.get(i);
}
rect.y += HEIGHT;
}
return null;
}
private List<String> getVisibleCategories() {
List<String> categories = new ArrayList<>(fCategories);
categories.retainAll(fMarkers.keySet());
return categories;
}
}