| /******************************************************************************* |
| * 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; |
| } |
| } |