blob: dcb63d5e699b04ae0402635d720a78461f73eb87 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2019 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:
* Alvaro Sanchez-Leon - Initial API and implementation
* Patrick Tasse - Refactoring
*******************************************************************************/
package org.eclipse.tracecompass.tmf.ui.widgets.timegraph.dialogs;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.ColorDialog;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Scale;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.internal.tmf.ui.ITmfImageConstants;
import org.eclipse.tracecompass.internal.tmf.ui.Messages;
import org.eclipse.tracecompass.internal.tmf.ui.util.TimeGraphStyleUtil;
import org.eclipse.tracecompass.tmf.core.model.StyleProperties;
import org.eclipse.tracecompass.tmf.ui.colors.ColorUtils;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.ITimeGraphPresentationProvider;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.StateItem;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEventStyleStrings;
import com.google.common.collect.Collections2;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
/**
* Legend for the colors used in the time graph view
*
* @version 1.0
* @author Alvaro Sanchez-Leon
* @author Patrick Tasse
*/
public class TimeGraphLegend extends TitleAreaDialog {
private static final ImageDescriptor RESET_IMAGE = Activator.getDefault().getImageDescripterFromPath(ITmfImageConstants.IMG_RESET_BUTTON);
private final ITimeGraphPresentationProvider fProvider;
private final LocalResourceManager fResourceManager = new LocalResourceManager(JFaceResources.getResources());
private Composite fInnerComposite;
/**
* Open the time graph legend window
*
* @param parent
* The parent shell
* @param provider
* The presentation provider
*/
public static void open(Shell parent, ITimeGraphPresentationProvider provider) {
(new TimeGraphLegend(parent, provider)).open();
}
/**
* Standard constructor
*
* @param parent
* The parent shell
* @param provider
* The presentation provider
*/
public TimeGraphLegend(Shell parent, ITimeGraphPresentationProvider provider) {
super(parent);
fProvider = provider;
this.setShellStyle(getShellStyle() | SWT.RESIZE);
}
/**
* Gets the Presentation Provider
*
* @return the presentation provider
* @since 3.3
*/
protected final ITimeGraphPresentationProvider getPresentationProvider() {
return fProvider;
}
@Override
protected Control createDialogArea(Composite parent) {
Composite composite = (Composite) super.createDialogArea(parent);
addStateGroups(composite);
setTitle(Messages.TmfTimeLegend_LEGEND);
setDialogHelpAvailable(false);
setHelpAvailable(false);
// Set the minimum size to avoid 0-sized legends from user resize
parent.getShell().setMinimumSize(150, 150);
composite.addDisposeListener((e) -> {
fResourceManager.dispose();
});
return composite;
}
@Override
protected Point getInitialSize() {
Point initialSize = super.getInitialSize();
Composite innerComposite = fInnerComposite;
/*
* If shell initial size is taller than available area, use 2 columns.
*/
if (initialSize.y > Display.getDefault().getClientArea().height && innerComposite != null) {
// Needed to make sure resize listener gets the right shell size
getShell().layout();
getGridLayouts(innerComposite).forEach(gl -> gl.numColumns = 2);
initialSize = super.getInitialSize();
// Needed to make sure vertical scroll bar appears
Display.getDefault().asyncExec(() -> innerComposite.getParent().layout());
}
return initialSize;
}
/**
* Creates a states group
*
* @param composite
* the parent composite
* @since 3.3
*/
private void addStateGroups(Composite composite) {
StateItem[] stateTable = fProvider.getStateTable();
if (stateTable == null) {
return;
}
List<StateItem> stateItems = Arrays.asList(stateTable);
Collection<StateItem> linkStates = Collections2.filter(stateItems, TimeGraphLegend::isLinkState);
ScrolledComposite sc = new ScrolledComposite(composite, SWT.V_SCROLL | SWT.H_SCROLL);
sc.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
sc.setExpandHorizontal(true);
sc.setExpandVertical(true);
sc.setLayout(GridLayoutFactory.swtDefaults().margins(200, 0).create());
Composite innerComposite = new Composite(sc, SWT.NONE);
fInnerComposite = innerComposite;
GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true);
innerComposite.setLayoutData(gd);
innerComposite.setLayout(new GridLayout());
innerComposite.addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
/*
* Find the highest number of columns that fits in the new width
*/
Point size = innerComposite.getSize();
List<GridLayout> gridLayouts = getGridLayouts(innerComposite);
Point minSize = new Point(0, 0);
for (int columns = 8; columns > 0; columns--) {
final int numColumns = columns;
gridLayouts.forEach(gl -> gl.numColumns = numColumns);
minSize = innerComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT);
if (minSize.x <= size.x) {
break;
}
}
sc.setMinSize(0, minSize.y);
}
});
sc.setContent(innerComposite);
createStatesGroup(innerComposite);
createLinkGroup(linkStates, innerComposite);
sc.setMinSize(0, innerComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT).y);
}
/**
* Creates a states group
*
* @param composite
* the parent composite
* @since 3.3
*/
protected void createStatesGroup(Composite composite) {
String stateTypeName = fProvider.getStateTypeName();
StringBuilder buffer = new StringBuilder();
if (!stateTypeName.isEmpty()) {
buffer.append(stateTypeName);
buffer.append(" "); //$NON-NLS-1$
}
buffer.append(Messages.TmfTimeLegend_StateTypeName);
StateItem[] stateTable = fProvider.getStateTable();
List<StateItem> stateItems = stateTable != null ? Arrays.asList(stateTable) : Collections.emptyList();
Multimap<String, StateItem> groupedStateItems = LinkedHashMultimap.create();
for (StateItem stateItem : stateItems) {
if (!isLinkState(stateItem)) {
Object group = stateItem.getStyleMap().get(StyleProperties.STYLE_GROUP);
if (group instanceof String) {
groupedStateItems.put((String) group, stateItem);
} else {
groupedStateItems.put(buffer.toString(), stateItem);
}
}
}
for (String groupName : groupedStateItems.keySet()) {
Collection<StateItem> groupItems = groupedStateItems.get(groupName);
createGroup(composite, groupName, groupItems);
}
}
private static List<GridLayout> getGridLayouts(Composite innerComposite) {
List<GridLayout> gridLayouts = new ArrayList<>();
if (innerComposite == null) {
return gridLayouts;
}
Arrays.asList(innerComposite.getChildren()).forEach(control -> {
if (control instanceof Composite) {
Layout layout = ((Composite) control).getLayout();
if (layout instanceof GridLayout) {
gridLayouts.add((GridLayout) layout);
}
}
});
return gridLayouts;
}
private void createLinkGroup(Collection<StateItem> linkStates, Composite composite) {
if (linkStates.isEmpty()) {
return;
}
createGroup(composite, fProvider.getLinkTypeName(), linkStates);
}
private void createGroup(Composite parent, String name, Collection<StateItem> stateItems) {
Group group = new Group(parent, SWT.NONE);
group.setText(name);
group.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false));
GridLayout layout = new GridLayout(1, true);
layout.marginWidth = 10;
layout.marginBottom = 10;
group.setLayout(layout);
for (StateItem stateItem : stateItems) {
new LegendEntry(group, stateItem);
}
}
/**
* Test whether a state item is a link state or not
*
* @param si
* The state item
* @return True if the state item is a link state, false otherwise
* @since 4.0
*/
protected static boolean isLinkState(StateItem si) {
Object itemType = si.getStyleMap().getOrDefault(ITimeEventStyleStrings.itemTypeProperty(), ITimeEventStyleStrings.stateType());
return itemType instanceof String && ((String) itemType).equals(ITimeEventStyleStrings.linkType());
}
@Override
protected void configureShell(Shell shell) {
super.configureShell(shell);
shell.setText(Messages.TmfTimeLegend_LEGEND);
}
@Override
protected void createButtonsForButtonBar(Composite parent) {
createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL,
true);
}
/**
* Widget for a legend entry has a color chooser, a label, a width and a reset
* button
*
* @author Matthew Khouzam
* @since 3.3
*/
protected class LegendEntry extends Composite {
/**
* ID to identify a control as part of a given entry
*/
private static final String LEGEND_ENTRY_KEY = "legend.entry.key"; //$NON-NLS-1$
private final Swatch fBar;
private final Scale fScale;
private final Button fReset;
/**
* Constructor
*
* @param parent
* parent composite
* @param si
* the state item
*/
public LegendEntry(Composite parent, StateItem si) {
super(parent, SWT.NONE);
String fillColorKey = TimeGraphStyleUtil.getPreferenceName(fProvider, si, StyleProperties.BACKGROUND_COLOR);
String heightFactorKey = TimeGraphStyleUtil.getPreferenceName(fProvider, si, StyleProperties.HEIGHT);
IPreferenceStore store = TimeGraphStyleUtil.getStore();
TimeGraphStyleUtil.loadValue(fProvider, si);
String name = si.getStateString();
setLayout(GridLayoutFactory.swtDefaults().numColumns(4).create());
fBar = new Swatch(this, si.getStateColor());
fBar.setData(LEGEND_ENTRY_KEY, name);
fBar.setToolTipText(Messages.TimeGraphLegend_swatchClick);
fBar.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
Shell shell = new Shell();
ColorDialog cd = new ColorDialog(shell, SWT.NONE);
cd.setRGB(fBar.fColor.getRGB());
RGB color = cd.open();
if (color != null) {
store.setValue(fillColorKey, ColorUtils.toHexColor(color.red, color.green, color.blue));
fBar.setColor(color);
si.setStateColor(color);
fProvider.refresh();
fReset.setEnabled(true);
}
}
});
fBar.addMouseTrackListener(new MouseTrackListener() {
@Override
public void mouseHover(MouseEvent e) {
// Do nothing
}
@Override
public void mouseExit(MouseEvent e) {
Shell shell = parent.getShell();
Cursor old = shell.getCursor();
shell.setCursor(new Cursor(e.display, SWT.CURSOR_ARROW));
if (old != null) {
old.dispose();
}
}
@Override
public void mouseEnter(MouseEvent e) {
Shell shell = parent.getShell();
Cursor old = shell.getCursor();
shell.setCursor(new Cursor(e.display, SWT.CURSOR_HAND));
if (old != null) {
old.dispose();
}
}
});
fBar.setLayoutData(GridDataFactory.swtDefaults().hint(30, 20).create());
CLabel label = new CLabel(this, SWT.NONE) {
@Override
protected String shortenText(GC gc, String t, int w) {
String text = super.shortenText(gc, t, w);
setToolTipText(t.equals(text) ? null : t);
return text;
}
};
label.setData(LEGEND_ENTRY_KEY, name);
label.setText(name);
label.setLayoutData(GridDataFactory.fillDefaults().hint(160, SWT.DEFAULT).align(SWT.FILL, SWT.CENTER).grab(true, false).create());
fScale = new Scale(this, SWT.NONE);
fScale.setMaximum(100);
fScale.setMinimum(1);
fScale.setSelection((int) (100 * si.getStateHeightFactor()));
fScale.setToolTipText(Messages.TimeGraphLegend_widthTooltip);
fScale.setData(LEGEND_ENTRY_KEY, name);
fScale.addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(SelectionEvent e) {
float newWidth = fScale.getSelection() * 0.01f;
store.setValue(heightFactorKey, newWidth);
si.getStyleMap().put(StyleProperties.HEIGHT, newWidth);
fProvider.refresh();
fReset.setEnabled(true);
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
// do nothing
}
});
fScale.setLayoutData(GridDataFactory.swtDefaults().hint(120, SWT.DEFAULT).create());
fReset = new Button(this, SWT.FLAT);
fReset.setData(LEGEND_ENTRY_KEY, name);
fReset.addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(SelectionEvent e) {
si.reset();
store.setToDefault(heightFactorKey);
store.setToDefault(fillColorKey);
fBar.setColor(si.getStateColor());
fScale.setSelection((int) (100 * si.getStateHeightFactor()));
fProvider.refresh();
fReset.setEnabled(false);
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
// do nothing
}
});
fReset.setToolTipText(Messages.TimeGraphLegend_resetTooltip);
fReset.setImage(RESET_IMAGE.createImage());
fReset.setLayoutData(GridDataFactory.swtDefaults().align(SWT.END, SWT.CENTER).create());
if (store.getString(fillColorKey).equals(store.getDefaultString(fillColorKey)) &&
store.getFloat(heightFactorKey) == store.getDefaultFloat(heightFactorKey)) {
fReset.setEnabled(false);
}
}
@Override
public void dispose() {
fReset.getImage().dispose();
super.dispose();
}
}
private class Swatch extends Canvas {
private Color fColor;
public Swatch(Composite parent, RGB rgb) {
super(parent, SWT.FLAT);
fColor = fResourceManager.createColor(rgb);
setForeground(fColor);
addListener(SWT.Paint, event -> draw(event.gc));
}
public void setColor(RGB rgb) {
if (rgb != null) {
fColor = fResourceManager.createColor(rgb);
setForeground(fColor);
redraw();
}
}
private void draw(GC gc) {
Rectangle r = getClientArea();
gc.setBackground(fColor);
gc.fillRectangle(r);
gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
gc.setLineWidth(2);
gc.drawRectangle(1, 1, r.width - 2, r.height - 2);
}
}
}