1 /*
   2  * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * Copyright (c) 2019, Red Hat Inc. All rights reserved.
   4  *
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * The contents of this file are subject to the terms of either the Universal Permissive License
   8  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   9  *
  10  * or the following license:
  11  *
  12  * Redistribution and use in source and binary forms, with or without modification, are permitted
  13  * provided that the following conditions are met:
  14  *
  15  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  16  * and the following disclaimer.
  17  *
  18  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  19  * conditions and the following disclaimer in the documentation and/or other materials provided with
  20  * the distribution.
  21  *
  22  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  23  * endorse or promote products derived from this software without specific prior written permission.
  24  *
  25  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  26  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  27  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  28  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  29  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  31  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  32  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  33  */
  34 package org.openjdk.jmc.flightrecorder.ui.common;
  35 
  36 import java.awt.Color;
  37 import java.io.FileInputStream;
  38 import java.io.InputStream;
  39 import java.util.ArrayList;
  40 import java.util.Arrays;
  41 import java.util.Collection;
  42 import java.util.Collections;
  43 import java.util.Comparator;
  44 import java.util.HashMap;
  45 import java.util.HashSet;
  46 import java.util.Iterator;
  47 import java.util.List;
  48 import java.util.Map;
  49 import java.util.Objects;
  50 import java.util.Optional;
  51 import java.util.Set;
  52 import java.util.function.Consumer;
  53 import java.util.function.Predicate;
  54 import java.util.function.Supplier;
  55 import java.util.regex.Matcher;
  56 import java.util.regex.Pattern;
  57 import java.util.stream.Collectors;
  58 import java.util.stream.Stream;
  59 
  60 import org.eclipse.core.runtime.IAdaptable;
  61 import org.eclipse.jface.action.Action;
  62 import org.eclipse.jface.action.IAction;
  63 import org.eclipse.jface.action.IMenuListener;
  64 import org.eclipse.jface.action.IMenuManager;
  65 import org.eclipse.jface.action.MenuManager;
  66 import org.eclipse.jface.action.Separator;
  67 import org.eclipse.jface.dialogs.InputDialog;
  68 import org.eclipse.jface.resource.ImageDescriptor;
  69 import org.eclipse.jface.resource.JFaceResources;
  70 import org.eclipse.jface.viewers.ColumnViewer;
  71 import org.eclipse.jface.viewers.IBaseLabelProvider;
  72 import org.eclipse.jface.viewers.StyledCellLabelProvider;
  73 import org.eclipse.jface.viewers.ViewerCell;
  74 import org.eclipse.jface.window.ToolTip;
  75 import org.eclipse.jface.window.Window;
  76 import org.eclipse.jface.wizard.Wizard;
  77 import org.eclipse.jface.wizard.WizardDialog;
  78 import org.eclipse.jface.wizard.WizardPage;
  79 import org.eclipse.swt.SWT;
  80 import org.eclipse.swt.custom.CTabFolder;
  81 import org.eclipse.swt.custom.CTabItem;
  82 import org.eclipse.swt.events.SelectionAdapter;
  83 import org.eclipse.swt.events.SelectionEvent;
  84 import org.eclipse.swt.graphics.GC;
  85 import org.eclipse.swt.graphics.Image;
  86 import org.eclipse.swt.graphics.ImageData;
  87 import org.eclipse.swt.layout.FillLayout;
  88 import org.eclipse.swt.layout.GridData;
  89 import org.eclipse.swt.layout.GridLayout;
  90 import org.eclipse.swt.widgets.Button;
  91 import org.eclipse.swt.widgets.Composite;
  92 import org.eclipse.swt.widgets.Control;
  93 import org.eclipse.swt.widgets.Display;
  94 import org.eclipse.swt.widgets.Event;
  95 import org.eclipse.swt.widgets.FileDialog;
  96 import org.eclipse.swt.widgets.Label;
  97 import org.eclipse.ui.forms.widgets.Form;
  98 import org.eclipse.ui.forms.widgets.FormText;
  99 import org.eclipse.ui.forms.widgets.FormToolkit;
 100 import org.openjdk.jmc.common.item.IAggregator;
 101 import org.openjdk.jmc.common.item.IAttribute;
 102 import org.openjdk.jmc.common.item.ICanonicalAccessorFactory;
 103 import org.openjdk.jmc.common.item.IItem;
 104 import org.openjdk.jmc.common.item.IItemCollection;
 105 import org.openjdk.jmc.common.item.IItemFilter;
 106 import org.openjdk.jmc.common.item.IItemIterable;
 107 import org.openjdk.jmc.common.item.IItemQuery;
 108 import org.openjdk.jmc.common.item.IMemberAccessor;
 109 import org.openjdk.jmc.common.item.IType;
 110 import org.openjdk.jmc.common.item.ItemFilters;
 111 import org.openjdk.jmc.common.item.ItemToolkit;
 112 import org.openjdk.jmc.common.unit.IQuantity;
 113 import org.openjdk.jmc.common.unit.IRange;
 114 import org.openjdk.jmc.common.unit.KindOfQuantity;
 115 import org.openjdk.jmc.common.unit.QuantitiesToolkit;
 116 import org.openjdk.jmc.common.unit.QuantityRange;
 117 import org.openjdk.jmc.common.unit.RangeContentType;
 118 import org.openjdk.jmc.common.unit.UnitLookup;
 119 import org.openjdk.jmc.common.util.ColorToolkit;
 120 import org.openjdk.jmc.common.util.CompositeKey;
 121 import org.openjdk.jmc.flightrecorder.JfrAttributes;
 122 import org.openjdk.jmc.flightrecorder.jdk.JdkAggregators;
 123 import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes;
 124 import org.openjdk.jmc.flightrecorder.jdk.JdkFilters;
 125 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs;
 126 import org.openjdk.jmc.flightrecorder.rules.Result;
 127 import org.openjdk.jmc.flightrecorder.rules.Severity;
 128 import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI;
 129 import org.openjdk.jmc.flightrecorder.ui.IPageContainer;
 130 import org.openjdk.jmc.flightrecorder.ui.ItemCollectionToolkit;
 131 import org.openjdk.jmc.flightrecorder.ui.ItemIterableToolkit;
 132 import org.openjdk.jmc.flightrecorder.ui.PageManager;
 133 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram.CompositeKeyHistogramBuilder;
 134 import org.openjdk.jmc.flightrecorder.ui.common.ItemList.ItemListBuilder;
 135 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages;
 136 import org.openjdk.jmc.flightrecorder.ui.overview.ResultOverview;
 137 import org.openjdk.jmc.flightrecorder.ui.pages.itemhandler.ItemHandlerPage;
 138 import org.openjdk.jmc.flightrecorder.ui.pages.itemhandler.ItemHandlerPage.ItemHandlerUiStandIn;
 139 import org.openjdk.jmc.flightrecorder.ui.selection.IFilterFlavor;
 140 import org.openjdk.jmc.flightrecorder.ui.selection.SelectionStore.SelectionStoreEntry;
 141 import org.openjdk.jmc.flightrecorder.ui.selection.SelectionStoreActionToolkit;
 142 import org.openjdk.jmc.ui.charts.AWTChartToolkit.IColorProvider;
 143 import org.openjdk.jmc.ui.charts.ChartFilterControlBar;
 144 import org.openjdk.jmc.ui.charts.IQuantitySeries;
 145 import org.openjdk.jmc.ui.charts.ISpanSeries;
 146 import org.openjdk.jmc.ui.charts.IXDataRenderer;
 147 import org.openjdk.jmc.ui.charts.QuantitySeries;
 148 import org.openjdk.jmc.ui.charts.RendererToolkit;
 149 import org.openjdk.jmc.ui.charts.SpanRenderer;
 150 import org.openjdk.jmc.ui.charts.SubdividedQuantityRange;
 151 import org.openjdk.jmc.ui.charts.XYChart;
 152 import org.openjdk.jmc.ui.charts.XYDataRenderer;
 153 import org.openjdk.jmc.ui.charts.XYQuantities;
 154 import org.openjdk.jmc.ui.column.ColumnMenusFactory;
 155 import org.openjdk.jmc.ui.column.TableSettings;
 156 import org.openjdk.jmc.ui.column.TableSettings.ColumnSettings;
 157 import org.openjdk.jmc.ui.handlers.ActionToolkit;
 158 import org.openjdk.jmc.ui.handlers.MCContextMenuManager;
 159 import org.openjdk.jmc.ui.misc.ChartCanvas;
 160 import org.openjdk.jmc.ui.misc.ChartTextCanvas;
 161 import org.openjdk.jmc.ui.misc.CompositeToolkit;
 162 import org.openjdk.jmc.ui.misc.DisplayToolkit;
 163 import org.openjdk.jmc.ui.misc.FilterEditor;
 164 import org.openjdk.jmc.ui.misc.FilterEditor.AttributeValueProvider;
 165 import org.openjdk.jmc.ui.misc.OverlayImageDescriptor;
 166 import org.openjdk.jmc.ui.misc.SWTColorToolkit;
 167 
 168 public class DataPageToolkit {
 169 
 170         public static final IColorProvider<IItem> ITEM_COLOR = item -> TypeLabelProvider
 171                         .getColorOrDefault(item.getType().getIdentifier());
 172 
 173         public static final IColorProvider<IItem> getAttributeValueColor(final IAttribute<?> attribute) {
 174                 return new IColorProvider<IItem>() {
 175 
 176                         @Override
 177                         public Color getColor(IItem item) {
 178                                 IMemberAccessor<?, IItem> accessor = attribute.getAccessor(ItemToolkit.getItemType(item));
 179                                 Object attributeValue = accessor != null ? accessor.getMember(item) : null;
 180                                 // FIXME: Should we include the type or not?
 181                                 return attributeValue != null
 182                                                 ? TypeLabelProvider.getColorOrDefault(attribute.getIdentifier() + "=" + attributeValue + "(" //$NON-NLS-1$ //$NON-NLS-2$
 183                                                                 + item.getType().getIdentifier() + ")") //$NON-NLS-1$
 184                                                 : ITEM_COLOR.getColor(item);
 185                         }
 186                 };
 187         }
 188 
 189         private static final Map<String, Color> FIELD_COLOR_MAP = new HashMap<>();
 190         private static final Map<String, Integer> DEFAULT_COLUMNS_ORDER;
 191 
 192         static {
 193 
 194                 // FIXME: Create FieldAppearance class, similar to TypeAppearence?
 195                 FIELD_COLOR_MAP.put(JdkAttributes.MACHINE_TOTAL.getIdentifier(), new Color(255, 128, 0));
 196                 FIELD_COLOR_MAP.put(JdkAttributes.JVM_SYSTEM.getIdentifier(), new Color(128, 128, 128));
 197                 FIELD_COLOR_MAP.put(JdkAttributes.JVM_USER.getIdentifier(), new Color(0, 0, 255));
 198                 FIELD_COLOR_MAP.put(JdkAttributes.JVM_TOTAL.getIdentifier(), new Color(64, 64, 191));
 199 
 200                 // FIXME: Handle ColorProvider and combined events
 201                 Map<String, Integer> columnsOrderMap = new HashMap<>();
 202                 columnsOrderMap.put(createColumnId(JfrAttributes.START_TIME), 1);
 203                 columnsOrderMap.put(createColumnId(JfrAttributes.DURATION), 2);
 204                 columnsOrderMap.put(createColumnId(JfrAttributes.END_TIME), 3);
 205                 columnsOrderMap.put(createColumnId(JfrAttributes.EVENT_THREAD), 4);
 206                 DEFAULT_COLUMNS_ORDER = Collections.unmodifiableMap(columnsOrderMap);
 207         }
 208 
 209         public static final Color ALLOCATION_COLOR = new Color(64, 144, 230);
 210 
 211         public static final String FORM_TOOLBAR_PAGE_RESULTS = "pageResults"; //$NON-NLS-1$
 212         public static final String FORM_TOOLBAR_PAGE_SETUP = "pageSetup"; //$NON-NLS-1$
 213         public static final String FORM_TOOLBAR_PAGE_NAV = "pageNav"; //$NON-NLS-1$
 214 
 215         public static final String RESULT_ACTION_ID = "resultAction"; //$NON-NLS-1$
 216 
 217         public static Color getFieldColor(String fieldId) {
 218                 return FIELD_COLOR_MAP.getOrDefault(fieldId, ColorToolkit.getDistinguishableColor(fieldId));
 219         }
 220 
 221         public static Color getFieldColor(IAttribute<?> attribute) {
 222                 return getFieldColor(attribute.getIdentifier());
 223         }
 224 
 225         public static TableSettings createTableSettingsByOrderByAndColumnsWithDefaultOrdering(
 226                 final String orderBy, final Collection<ColumnSettings> columns) {
 227                 final Stream<ColumnSettings> defaultOrderColumns = columns.stream()
 228                                 .filter(c -> DEFAULT_COLUMNS_ORDER.containsKey(c.getId())).filter(c -> !c.isHidden())
 229                                 .sorted((c1, c2) -> Integer.compare(DEFAULT_COLUMNS_ORDER.get(c1.getId()),
 230                                                 DEFAULT_COLUMNS_ORDER.get(c2.getId())));
 231                 final Stream<ColumnSettings> naturalOrderColumns = columns.stream()
 232                                 .filter(c -> !DEFAULT_COLUMNS_ORDER.containsKey(c.getId()))
 233                                 .sorted((c1, c2) -> String.CASE_INSENSITIVE_ORDER.compare(c1.getId(), c2.getId()));
 234                 final List<ColumnSettings> resultColumns = Stream.concat(defaultOrderColumns, naturalOrderColumns)
 235                                 .collect(Collectors.toList());
 236                 return new TableSettings(orderBy, resultColumns);
 237         }
 238 
 239         public static TableSettings createTableSettingsByAllAndVisibleColumns(
 240                 final Collection<String> allColumns, final Collection<String> visibleColumns) {
 241                 final List<ColumnSettings> defaultListCols = new ArrayList<>();
 242                 for (String columnId : allColumns) {
 243                         defaultListCols.add(new ColumnSettings(columnId, !visibleColumns.contains(columnId), null, null));
 244                 }
 245                 return createTableSettingsByOrderByAndColumnsWithDefaultOrdering(null, defaultListCols);
 246         }
 247 
 248         private static String createColumnId(IAttribute<?> attr) {
 249                 return new StringBuilder().append(attr.getIdentifier()).append(":") //$NON-NLS-1$
 250                                 .append(attr.getContentType().getIdentifier()).toString();
 251         }
 252 
 253         public static IAction createAttributeCheckAction(IAttribute<?> attribute, Consumer<Boolean> onChange) {
 254                 return createCheckAction(attribute.getName(), attribute.getDescription(), attribute.getIdentifier(),
 255                                 getFieldColor(attribute), onChange);
 256         }
 257 
 258         public static IAction createTypeCheckAction(
 259                 String actionId, String typeId, String name, String description, Consumer<Boolean> onChange) {
 260                 return createCheckAction(name, description, actionId, TypeLabelProvider.getColorOrDefault(typeId), onChange);
 261         }
 262 
 263         public static IAction createAggregatorCheckAction(
 264                 IAggregator<?, ?> aggregator, String id, Color color, Consumer<Boolean> onChange) {
 265                 return createCheckAction(aggregator.getName(), aggregator.getDescription(), id, color, onChange);
 266         }
 267 
 268         public static IAction createCheckAction(
 269                 String name, String description, String id, Color color, Consumer<Boolean> onChange) {
 270                 return createCheckAction(name, description, id,
 271                                 SWTColorToolkit.getColorThumbnailDescriptor(SWTColorToolkit.asRGB(color)), onChange);
 272         }
 273 
 274         public static IAction createCheckAction(
 275                 String name, String description, String id, ImageDescriptor icon, Consumer<Boolean> onChange) {
 276                 return ActionToolkit.checkAction(onChange, name, description, icon, id);
 277         }
 278 
 279         public static Optional<IXDataRenderer> buildLinesRow(
 280                 String title, String description, IItemCollection items, boolean fill, IItemQuery query,
 281                 Predicate<IAttribute<IQuantity>> attributeFilter, IQuantity includeLow, IQuantity includeHigh) {
 282                 XYDataRenderer renderer = includeHigh != null
 283                                 ? new XYDataRenderer(includeLow, includeHigh, true, title, description)
 284                                 : new XYDataRenderer(includeLow, title, description);
 285                 IItemCollection filteredItemsSupplier = items.apply(query.getFilter());
 286                 Stream<IAttribute<IQuantity>> attributes = getQuantityAttributes(query);
 287                 if (attributeFilter != null) {
 288                         attributes = attributes.filter(attributeFilter);
 289                 }
 290                 if (DataPageToolkit.addEndTimeLines(renderer, filteredItemsSupplier, fill, attributes)) {
 291                         return Optional.of(new ItemRow(title, description, renderer, filteredItemsSupplier));
 292                 }
 293                 return Optional.empty();
 294         }
 295 
 296         /**
 297          * @param q
 298          *            A query containing only {@code IAttribute<IQuantity>} attributes. Queries
 299          *            containing non-quantity attributes are not supported and may cause
 300          *            ClassCastExceptions later when the attributes are used.
 301          * @return a stream of the query attributes
 302          */
 303         /*
 304          * FIXME: JMC-5125 - This cast chain is scary and should be reworked.
 305          * 
 306          * If the query contains any non-quantity attributes then there will be a ClassCastException
 307          * later when the attributes are used to extract values.
 308          */
 309         @SuppressWarnings({"unchecked", "rawtypes"})
 310         public static Stream<IAttribute<IQuantity>> getQuantityAttributes(IItemQuery q) {
 311                 return (Stream) q.getAttributes().stream();
 312         }
 313 
 314         public static void setChart(ChartCanvas canvas, XYChart chart, Consumer<IItemCollection> selectionListener) {
 315                 setChart(canvas, chart, selectionListener, null);
 316         }
 317 
 318         public static void setChart(ChartTextCanvas canvas, XYChart chart, Consumer<IItemCollection> selectionListener) {
 319                 setChart(canvas, chart, selectionListener, null);
 320         }
 321 
 322         public static void setChart(
 323                 ChartCanvas canvas, XYChart chart, Consumer<IItemCollection> selectionListener,
 324                 Consumer<IRange<IQuantity>> selectRangeConsumer) {
 325                 IMenuManager contextMenu = canvas.getContextMenu();
 326                 contextMenu.removeAll();
 327                 canvas.getContextMenu().add(new Action(Messages.CHART_ZOOM_TO_SELECTED_RANGE) {
 328                         @Override
 329                         public void run() {
 330                                 IQuantity selectionStart = chart.getSelectionStart();
 331                                 IQuantity selectionEnd = chart.getSelectionEnd();
 332                                 if (selectionStart == null || selectionEnd == null) {
 333                                         chart.clearVisibleRange();
 334                                 } else {
 335                                         chart.setVisibleRange(selectionStart, selectionEnd);
 336                                 }
 337                                 canvas.redrawChart();
 338                         }
 339                 });
 340 
 341                 canvas.setSelectionListener(() -> {
 342                         selectionListener.accept(ItemRow.getRangeSelection(chart, JfrAttributes.LIFETIME));
 343                         IQuantity start = chart.getSelectionStart();
 344                         IQuantity end = chart.getSelectionEnd();
 345                         if (selectRangeConsumer != null) {
 346                                 selectRangeConsumer
 347                                                 .accept(start != null && end != null ? QuantityRange.createWithEnd(start, end) : null);
 348                         }
 349                 });
 350                 canvas.setChart(chart);
 351         }
 352 
 353         public static void setChart(
 354                 ChartTextCanvas canvas, XYChart chart, Consumer<IItemCollection> selectionListener,
 355                 Consumer<IRange<IQuantity>> selectRangeConsumer) {
 356                 IMenuManager contextMenu = canvas.getContextMenu();
 357                 contextMenu.removeAll();
 358                 canvas.getContextMenu().add(new Action(Messages.CHART_ZOOM_TO_SELECTED_RANGE) {
 359                         @Override
 360                         public void run() {
 361                                 IQuantity selectionStart = chart.getSelectionStart();
 362                                 IQuantity selectionEnd = chart.getSelectionEnd();
 363                                 if (selectionStart == null || selectionEnd == null) {
 364                                         chart.clearVisibleRange();
 365                                 } else {
 366                                         chart.setVisibleRange(selectionStart, selectionEnd);
 367                                 }
 368                                 canvas.redrawChartText();
 369                         }
 370                 });
 371 
 372                 canvas.setSelectionListener(() -> {
 373                         selectionListener.accept(ItemRow.getRangeSelection(chart, JfrAttributes.LIFETIME));
 374                         IQuantity start = chart.getSelectionStart();
 375                         IQuantity end = chart.getSelectionEnd();
 376                         if (selectRangeConsumer != null) {
 377                                 selectRangeConsumer
 378                                                 .accept(start != null && end != null ? QuantityRange.createWithEnd(start, end) : null);
 379                         }
 380                 });
 381                 canvas.setChart(chart);
 382         }
 383 
 384         public static void setChart(
 385                 ChartCanvas canvas, XYChart chart, IAttribute<IQuantity> selectionAttribute,
 386                 Consumer<IItemCollection> selectionListener) {
 387                 IMenuManager contextMenu = canvas.getContextMenu();
 388                 contextMenu.removeAll();
 389                 canvas.setSelectionListener(() -> selectionListener.accept(ItemRow.getSelection(chart, selectionAttribute)));
 390                 canvas.setChart(chart);
 391         }
 392 
 393         /**
 394          * Only works for items that are either fully overlapping, or disjunct. Must be ensured by
 395          * client code.
 396          */
 397         private static class RangePayload implements IAdaptable {
 398                 IItem item;
 399                 IQuantity start;
 400                 IQuantity end;
 401                 double rangeInPixels;
 402 
 403                 RangePayload(IItem item, IQuantity start, IQuantity end, double rangeInPixels) {
 404                         this.item = item;
 405                         this.start = start;
 406                         this.end = end;
 407                         this.rangeInPixels = rangeInPixels;
 408                 }
 409 
 410                 void combineWith(IItem item, IQuantity start, IQuantity end, double rangeInPixels) {
 411                         if (this.start.compareTo(start) < 0) {
 412                                 // Will choose the item that starts last
 413                                 this.start = start;
 414                                 this.end = end;
 415                                 this.item = item;
 416                                 extendRangeInPixels(this.end.compareTo(start) > 0, rangeInPixels);
 417                         } else {
 418                                 extendRangeInPixels(end.compareTo(this.start) > 0, rangeInPixels);
 419                         }
 420                 }
 421 
 422                 void extendRangeInPixels(boolean overlapping, double rangeInPixels) {
 423                         this.rangeInPixels = overlapping ? Math.max(this.rangeInPixels, rangeInPixels)
 424                                         : this.rangeInPixels + rangeInPixels;
 425                 }
 426 
 427                 @Override
 428                 public <T> T getAdapter(Class<T> adapter) {
 429                         return IItem.class.isAssignableFrom(adapter) ? adapter.cast(item) : null;
 430                 }
 431         }
 432 
 433         private static ISpanSeries<RangePayload> rangeSeries(
 434                 IItemCollection events, IAttribute<IQuantity> startAttribute, IAttribute<IQuantity> endAttribute) {
 435                 return new ISpanSeries<RangePayload>() {
 436                         @Override
 437                         public XYQuantities<RangePayload[]> getQuantities(SubdividedQuantityRange xBucketRange) {
 438                                 SubdividedQuantityRange xRange = xBucketRange.copyWithPixelSubdividers();
 439                                 List<RangePayload> spanningPixels = new ArrayList<>();
 440                                 RangePayload[] pixelBuckets = new RangePayload[xRange.getNumSubdividers()];
 441                                 events.forEach(is -> {
 442                                         IMemberAccessor<IQuantity, IItem> startAccessor = startAttribute.getAccessor(is.getType());
 443                                         IMemberAccessor<IQuantity, IItem> endAccessor = endAttribute.getAccessor(is.getType());
 444                                         is.forEach(item -> {
 445                                                 IQuantity start = startAccessor.getMember(item);
 446                                                 IQuantity end = endAccessor.getMember(item);
 447                                                 int xPos = xRange.getFloorSubdivider(start);
 448                                                 int endPos = xRange.getFloorSubdivider(end);
 449                                                 if (xPos < pixelBuckets.length && endPos >= 0) {
 450                                                         // FIXME: If we have very short events (nanosecond scale) we can sometimes get a negative range.
 451                                                         double rangeInPixels = xRange.getPixel(end) - xRange.getPixel(start);
 452                                                         if (xPos != endPos) {
 453                                                                 spanningPixels.add(new RangePayload(item, start, end, rangeInPixels));
 454                                                         } else if (pixelBuckets[xPos] == null) {
 455                                                                 pixelBuckets[xPos] = new RangePayload(item, start, end, rangeInPixels);
 456                                                         } else {
 457                                                                 pixelBuckets[xPos].combineWith(item, start, end, rangeInPixels);
 458                                                         }
 459                                                 }
 460                                         });
 461                                 });
 462                                 RangePayload[] sorted = Stream
 463                                                 .concat(Stream.of(pixelBuckets).filter(Objects::nonNull), spanningPixels.stream())
 464                                                 .sorted(Comparator.comparing(r -> r.start)).toArray(RangePayload[]::new);
 465                                 // FIXME: Should make it possible to use the RangePayload[] directly instead
 466                                 List<IQuantity> starts = Stream.of(sorted).map(r -> r.start).collect(Collectors.toList());
 467                                 List<IQuantity> ends = Stream.of(sorted).map(r -> r.end).collect(Collectors.toList());
 468                                 return XYQuantities.create(sorted, starts, ends, xRange);
 469                         }
 470 
 471                         @Override
 472                         public IQuantity getStartX(RangePayload payload) {
 473                                 return payload.start;
 474                         }
 475                 };
 476         }
 477 
 478         public final static Color GC_BASE_COLOR = TypeLabelProvider.getColor(JdkTypeIDs.GC_PAUSE);
 479         private final static Color VM_OPERATIONS_BASE_COLOR = TypeLabelProvider.getColor(JdkTypeIDs.VM_OPERATIONS);
 480         private final static IColorProvider<RangePayload> GC_COLOR = payload -> adjustAlpha(GC_BASE_COLOR,
 481                         payload.rangeInPixels);
 482         private final static IColorProvider<RangePayload> APPLICATION_PAUSE_COLOR = payload -> adjustAlpha(
 483                         payload.item.getType().getIdentifier().equals(JdkTypeIDs.GC_PAUSE) ? GC_BASE_COLOR
 484                                         : VM_OPERATIONS_BASE_COLOR,
 485                         payload.rangeInPixels);
 486         public final static ImageDescriptor GC_LEGEND_ICON = new OverlayImageDescriptor(
 487                         SWTColorToolkit.getColorThumbnailDescriptor(SWTColorToolkit.asRGB(GC_BASE_COLOR)), false,
 488                         FlightRecorderUI.getDefault().getMCImageDescriptor("trash_overlay.png")); //$NON-NLS-1$
 489 
 490         /**
 491          * Return a color with alpha calculated from a fraction.
 492          *
 493          * @param color
 494          *            a base color
 495          * @param fraction
 496          *            A value where 0 gives the lowest alpha value and 1 gives the highest. Fractions
 497          *            above 1 are accepted and treated as 1. Negative fractions should not be used.
 498          * @return a color with RGB from the base color and an alpha value depending on the fraction
 499          */
 500         private static Color adjustAlpha(Color color, double fraction) {
 501                 return ColorToolkit.withAlpha(color, Math.min(200, (int) ((Math.max(0, fraction) + 0.15) * 255)));
 502         }
 503 
 504         public static ItemRow buildGcPauseRow(IItemCollection items) {
 505                 IItemCollection pauseEvents = items.apply(JdkFilters.GC_PAUSE);
 506                 ISpanSeries<RangePayload> gcBackdrop = rangeSeries(pauseEvents, JfrAttributes.START_TIME,
 507                                 JfrAttributes.END_TIME);
 508                 return new ItemRow(SpanRenderer.build(gcBackdrop, GC_COLOR), pauseEvents);
 509         }
 510 
 511         public static ItemRow buildApplicationPauseRow(IItemCollection items) {
 512                 IItemFilter vmOperationPauseFilter = ItemFilters.and(JdkFilters.VM_OPERATIONS,
 513                                 ItemFilters.equals(JdkAttributes.SAFEPOINT, true));
 514                 IItemCollection applicationPauses = items
 515                                 .apply(ItemFilters.or(JdkFilters.GC_PAUSE, JdkFilters.SAFE_POINTS, vmOperationPauseFilter));
 516                 ISpanSeries<RangePayload> pausesSeries = rangeSeries(applicationPauses, JfrAttributes.START_TIME,
 517                                 JfrAttributes.END_TIME);
 518                 return new ItemRow(SpanRenderer.build(pausesSeries, APPLICATION_PAUSE_COLOR), applicationPauses);
 519         }
 520 
 521         public static IXDataRenderer buildTimestampHistogramRenderer(
 522                 IItemCollection items, IAggregator<IQuantity, ?> aggregator, IAttribute<IQuantity> timestampAttribute,
 523                 Color color) {
 524                 IQuantitySeries<IQuantity[]> aggregatorSeries = BucketBuilder.aggregatorSeries(items, aggregator,
 525                                 timestampAttribute);
 526                 XYDataRenderer renderer = new XYDataRenderer(getKindOfQuantity(aggregator).getDefaultUnit().quantity(0),
 527                                 aggregator.getName(), aggregator.getDescription());
 528                 renderer.addBarChart(aggregator.getName(), aggregatorSeries, color);
 529                 return renderer;
 530         }
 531 
 532         public static IXDataRenderer buildTimestampHistogramRenderer(
 533                 IItemCollection items, IAggregator<IQuantity, ?> aggregator, Color color) {
 534                 return buildTimestampHistogramRenderer(items, aggregator, JfrAttributes.CENTER_TIME, color);
 535         }
 536 
 537         public static ItemRow buildTimestampHistogram(
 538                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> aggregator,
 539                 IAttribute<IQuantity> timestampAttribute, Color color) {
 540                 return new ItemRow(title, description,
 541                                 buildTimestampHistogramRenderer(items, aggregator, timestampAttribute, color), items);
 542         }
 543 
 544         public static ItemRow buildTimestampHistogram(
 545                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> aggregator, Color color) {
 546                 return new ItemRow(title, description, buildTimestampHistogramRenderer(items, aggregator, color), items);
 547         }
 548 
 549         public static ItemHistogram createDistinctItemsTable(
 550                 Composite parent, IItemCollection items, IItemQuery query, TableSettings settings) {
 551                 CompositeKeyHistogramBuilder histogramBuilder = new CompositeKeyHistogramBuilder();
 552                 for (IAttribute<?> attribute : query.getAttributes()) {
 553                         histogramBuilder.addKeyColumn(attribute);
 554                 }
 555                 for (IAggregator<?, ?> aggregator : query.getAggregators()) {
 556                         histogramBuilder.addColumn(aggregator.getName(), aggregator);
 557                 }
 558                 ItemHistogram table = histogramBuilder.buildWithoutBorder(parent, settings);
 559                 return table;
 560         }
 561 
 562         public static IBaseLabelProvider createTableHighlightProvider(Pattern highlightPattern, boolean isWarning) {
 563                 return new StyledCellLabelProvider() {
 564                         @Override
 565                         public void update(ViewerCell cell) {
 566                                 org.eclipse.swt.graphics.Color color = isWarning
 567                                                 ? new org.eclipse.swt.graphics.Color(Display.getCurrent(), 240, 120, 140)
 568                                                 : new org.eclipse.swt.graphics.Color(Display.getCurrent(), 255, 144, 4);
 569                                 String text = getText(cell.getElement(), cell.getColumnIndex());
 570                                 Matcher matcher = highlightPattern.matcher(text);
 571                                 if (matcher.find()) {
 572                                         cell.getViewerRow().setBackground(0, color);
 573                                         cell.getViewerRow().setBackground(1, color);
 574                                 }
 575                                 cell.setText(text);
 576                                 super.update(cell);
 577                         }
 578 
 579                         private String getText(Object element, int index) {
 580                                 Object key = AggregationGrid.getKey(element);
 581                                 Object[] keyElements = ((CompositeKey) key).getKeyElements();
 582                                 return keyElements[index].toString();
 583                         }
 584                 };
 585         }
 586 
 587         public static void addContextMenus(
 588                 IPageContainer pc, ItemHistogram h, String selectionName, IAction ... extraActions) {
 589                 MCContextMenuManager mm = MCContextMenuManager.create(h.getManager().getViewer().getControl());
 590                 ColumnMenusFactory.addDefaultMenus(h.getManager(), mm);
 591                 SelectionStoreActionToolkit.addSelectionStoreActions(pc.getSelectionStore(), h, selectionName, mm);
 592                 for (IAction action : extraActions) {
 593                         mm.add(action);
 594                 }
 595         }
 596 
 597         public static IXDataRenderer buildSizeRow(
 598                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> a, Color color,
 599                 IColorProvider<IItem> cp) {
 600                 return RendererToolkit.layers(buildSpanRenderer(items, cp),
 601                                 buildTimestampHistogram(title, description, items, a, color));
 602         }
 603 
 604         public static ItemRow buildDurationHistogram(
 605                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> a, Color color) {
 606                 IQuantitySeries<IQuantity[]> allocationSeries = BucketBuilder.aggregatorSeries(items, a,
 607                                 JfrAttributes.DURATION);
 608                 XYDataRenderer renderer = new XYDataRenderer(getKindOfQuantity(a).getDefaultUnit().quantity(0), title,
 609                                 description);
 610                 renderer.addBarChart(a.getName(), allocationSeries, color);
 611                 return new ItemRow(title, description, renderer, items);
 612         }
 613 
 614         public static ItemRow buildSizeHistogram(
 615                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> a, Color color, IAttribute<IQuantity> attribute) {
 616                 IQuantitySeries<IQuantity[]> allocationSeries = BucketBuilder.aggregatorSeries(items, a,
 617                                 JdkAttributes.IO_SIZE);
 618                 XYDataRenderer renderer = new XYDataRenderer(getKindOfQuantity(a).getDefaultUnit().quantity(0), title,
 619                                 description);
 620                 renderer.addBarChart(a.getName(), allocationSeries, color);
 621                 return new ItemRow(title, description, renderer, items);
 622         }
 623 
 624         public static IRange<IQuantity> buildSizeRange(IItemCollection items, boolean isSocket){
 625                 IQuantity end = null;
 626                 if(isSocket) {
 627                         end = QuantitiesToolkit.maxPresent(items.getAggregate(JdkAggregators.SOCKET_READ_LARGEST),
 628                                         items.getAggregate(JdkAggregators.SOCKET_WRITE_LARGEST));
 629                 } else {
 630                         end = QuantitiesToolkit.maxPresent(items.getAggregate(JdkAggregators.FILE_READ_LARGEST),
 631                                         items.getAggregate(JdkAggregators.FILE_WRITE_LARGEST));
 632                 }
 633                 end = end == null ? UnitLookup.BYTE.quantity(1024) : end;
 634                 return QuantityRange.createWithEnd(UnitLookup.BYTE.quantity(0), end);
 635         }
 636 
 637         // FIXME: Make something that can use something other than time as x-axis?
 638         public static IXDataRenderer buildSpanRenderer(IItemCollection pathItems, IColorProvider<IItem> cp) {
 639                 ISpanSeries<IItem> dataSeries = QuantitySeries.max(pathItems, JfrAttributes.START_TIME, JfrAttributes.END_TIME);
 640                 return SpanRenderer.withBoundaries(dataSeries, cp);
 641         }
 642 
 643         public static boolean addEndTimeLines(
 644                 XYDataRenderer renderer, IItemCollection items, boolean fill, Stream<IAttribute<IQuantity>> yAttributes) {
 645                 // FIXME: JMC-4520 - Handle multiple item iterables
 646                 Iterator<IItemIterable> ii = items.iterator();
 647                 if (ii.hasNext()) {
 648                         IItemIterable itemStream = ii.next();
 649                         IType<IItem> type = itemStream.getType();
 650                         // FIXME: A better way to ensure sorting by endTime
 651                         return yAttributes.peek(a -> addEndTimeLine(renderer, itemStream.iterator(), type, a, fill))
 652                                         .mapToLong(a -> 1L).sum() > 0;
 653                 }
 654                 return false;
 655         }
 656 
 657         public static void addEndTimeLine(
 658                 XYDataRenderer renderer, Iterator<? extends IItem> items, IType<IItem> type, IAttribute<IQuantity> yAttribute,
 659                 boolean fill) {
 660                 IQuantitySeries<?> qs = buildQuantitySeries(items, type, JfrAttributes.END_TIME, yAttribute);
 661                 renderer.addLineChart(yAttribute.getName(), qs, getFieldColor(yAttribute), fill);
 662         }
 663 
 664         public static IQuantitySeries<?> buildQuantitySeries(
 665                 Iterator<? extends IItem> items, IType<IItem> type, IAttribute<IQuantity> xAttribute,
 666                 IAttribute<IQuantity> yAttribute) {
 667                 IMemberAccessor<IQuantity, IItem> yAccessor = yAttribute.getAccessor(type);
 668                 if (yAccessor == null) {
 669                         throw new RuntimeException(yAttribute.getIdentifier() + " is not an attribute for " + type.getIdentifier()); //$NON-NLS-1$
 670                 }
 671                 return buildQuantitySeries(items, type, xAttribute, yAccessor);
 672         }
 673 
 674         public static IQuantitySeries<?> buildQuantitySeries(
 675                 Iterator<? extends IItem> items, IType<IItem> type, IAttribute<IQuantity> xAttribute,
 676                 IMemberAccessor<? extends IQuantity, IItem> yAccessor) {
 677                 IMemberAccessor<IQuantity, IItem> xAccessor = xAttribute.getAccessor(type);
 678                 return QuantitySeries.all(items, xAccessor, yAccessor);
 679         }
 680 
 681         public static void createChartTooltip(ChartCanvas chart) {
 682                 createChartTooltip(chart, ChartToolTipProvider::new);
 683         }
 684 
 685         public static void createChartTimestampTooltip(ChartCanvas chart) {
 686                 createChartTooltip(chart, JfrAttributes.START_TIME, JfrAttributes.END_TIME, JfrAttributes.DURATION,
 687                                 JfrAttributes.EVENT_TYPE, JfrAttributes.EVENT_STACKTRACE);
 688         }
 689 
 690         public static void createChartTooltip(ChartCanvas chart, IAttribute<?> ... excludedAttributes) {
 691                 createChartTooltip(chart, new HashSet<>(Arrays.asList(excludedAttributes)));
 692         }
 693 
 694         public static void createChartTooltip(ChartCanvas chart, Set<IAttribute<?>> excludedAttributes) {
 695                 createChartTooltip(chart, () -> new ChartToolTipProvider() {
 696                         @Override
 697                         protected Stream<IAttribute<?>> getAttributeStream(IType<IItem> type) {
 698                                 return type.getAttributes().stream().filter(a -> !excludedAttributes.contains(a));
 699                         }
 700                 });
 701         }
 702 
 703         public static void createChartTooltip(ChartCanvas chart, Supplier<ChartToolTipProvider> toolTipProviderSupplier) {
 704                 new ToolTip(chart) {
 705                         String html;
 706                         Map<String, Image> images;
 707 
 708                         @Override
 709                         protected boolean shouldCreateToolTip(Event event) {
 710                                 ChartToolTipProvider provider = toolTipProviderSupplier.get();
 711                                 chart.infoAt(provider, event.x, event.y);
 712                                 html = provider.getHTML();
 713                                 images = provider.getImages();
 714                                 return html != null;
 715                         }
 716 
 717                         @Override
 718                         protected Composite createToolTipContentArea(Event event, Composite parent) {
 719                                 FormText formText = CompositeToolkit.createInfoFormText(parent);
 720                                 for (Map.Entry<String, Image> imgEntry : images.entrySet()) {
 721                                         formText.setImage(imgEntry.getKey(), imgEntry.getValue());
 722                                 }
 723                                 formText.setText(html, true, false);
 724                                 return formText;
 725                         }
 726 
 727                 };
 728 
 729         }
 730 
 731         private static KindOfQuantity<?> getKindOfQuantity(IAggregator<IQuantity, ?> a) {
 732                 IType<? super IQuantity> ct = a.getValueType();
 733                 // FIXME: Refactor to avoid this cast
 734                 return ((KindOfQuantity<?>) ct);
 735         }
 736 
 737         public static Form createForm(Composite parent, FormToolkit toolkit, String title, Image img) {
 738                 Form form = toolkit.createForm(parent);
 739                 form.setText(title.replaceAll("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$
 740                 form.setImage(img);
 741                 toolkit.decorateFormHeading(form);
 742                 FillLayout fillLayout = new FillLayout();
 743                 fillLayout.marginHeight = 15;
 744                 fillLayout.marginWidth = 8;
 745                 form.getBody().setLayout(fillLayout);
 746                 form.getToolBarManager().add(new Separator(FORM_TOOLBAR_PAGE_RESULTS));
 747                 form.getToolBarManager().add(new Separator(FORM_TOOLBAR_PAGE_SETUP));
 748                 form.getToolBarManager().add(new Separator(FORM_TOOLBAR_PAGE_NAV));
 749                 return form;
 750         }
 751 
 752         public static class ShowResultAction extends Action {
 753 
 754                 private String[] topics;
 755                 private final IPageContainer pageContainer;
 756                 private volatile Severity maxSeverity;
 757                 private final List<Consumer<Result>> listeners = new ArrayList<>();
 758 
 759                 ShowResultAction(String title, int style, ImageDescriptor icon, Supplier<String> tooltip,
 760                                 IPageContainer pageContainer, String ... topics) {
 761                         super(title, style);
 762                         setImageDescriptor(icon);
 763                         setToolTipText(tooltip.get());
 764                         this.topics = topics;
 765                         this.pageContainer = pageContainer;
 766                         maxSeverity = pageContainer.getRuleManager().getMaxSeverity(topics);
 767                         for (String topic : topics) {
 768                                 Consumer<Result> listener = result -> {
 769                                         Severity severity = Severity.get(result.getScore());
 770                                         if (severity.compareTo(maxSeverity) > 0) {
 771                                                 maxSeverity = severity;
 772                                                 setImageDescriptor(getResultIcon(maxSeverity));
 773                                         } else if (severity.compareTo(maxSeverity) < 0) { // severity could be less than previous max
 774                                                 maxSeverity = pageContainer.getRuleManager().getMaxSeverity(topics);
 775                                         }
 776                                         setToolTipText(tooltip.get());
 777                                 };
 778                                 listeners.add(listener);
 779                                 pageContainer.getRuleManager().addResultListener(topic, listener);
 780                         }
 781                 }
 782 
 783                 private void removeListeners() {
 784                         listeners.forEach(l -> pageContainer.getRuleManager().removeResultListener(l));
 785                 }
 786 
 787                 @Override
 788                 public void run() {
 789                         pageContainer.showResults(topics);
 790                 }
 791         }
 792 
 793         public static void addRuleResultAction(
 794                 Form form, IPageContainer pageContainer, Supplier<String> tooltip, String[] topics) {
 795                 if (topics == null || topics.length == 0 || !FlightRecorderUI.getDefault().isAnalysisEnabled()) {
 796                         return;
 797                 }
 798                 ImageDescriptor icon = getResultIcon(pageContainer.getRuleManager().getMaxSeverity(topics));
 799                 ShowResultAction resultAction = new ShowResultAction(Messages.RULES_SHOW_RESULTS_ACTION, IAction.AS_PUSH_BUTTON,
 800                                 icon, tooltip, pageContainer, topics);
 801                 resultAction.setId(RESULT_ACTION_ID);
 802                 form.getToolBarManager().appendToGroup(DataPageToolkit.FORM_TOOLBAR_PAGE_RESULTS, resultAction);
 803                 form.getToolBarManager().update(true);
 804                 form.addDisposeListener(e -> resultAction.removeListeners());
 805         }
 806 
 807         private static ImageDescriptor getResultIcon(Severity severity) {
 808                 switch (severity) {
 809                 case OK:
 810                         return ResultOverview.ICON_OK;
 811                 case INFO:
 812                         return ResultOverview.ICON_INFO;
 813                 case WARNING:
 814                         return ResultOverview.ICON_WARNING;
 815                 case NA:
 816                         return ResultOverview.ICON_NA;
 817                 }
 818                 return null;
 819         }
 820 
 821         /**
 822          * Return a disabled Action.
 823          *
 824          * @param text
 825          *            text to be displayed by the MenuItem, and represent it as it's id.
 826          * @return an Action containing the desired text, which will be disabled in a UI component.
 827          */
 828         public static IAction disabledAction(String text) {
 829                 IAction disabledAction = new Action(text) {
 830                         @Override
 831                         public boolean isEnabled() {
 832                                 return false;
 833                         }
 834                 };
 835                 disabledAction.setId(text);
 836                 return disabledAction;
 837         }
 838 
 839         public static FilterEditor buildFilterSelector(
 840                 Composite parent, IItemFilter filter, IItemCollection items, Supplier<Stream<SelectionStoreEntry>> selections,
 841                 Consumer<IItemFilter> onSelect, boolean hasBorder) {
 842                 Supplier<Collection<IAttribute<?>>> attributeSupplier = () -> getPersistableAttributes(
 843                                 getAttributes(filter != null ? items.apply(filter) : items)).collect(Collectors.toList());
 844 
 845                 AttributeValueProvider valueSupplier = new AttributeValueProvider() {
 846                         @Override
 847                         public <V> V defaultValue(ICanonicalAccessorFactory<V> attribute) {
 848                                 return findValueForFilter(items, attribute);
 849                         }
 850                 };
 851 
 852                 FilterEditor editor = new FilterEditor(parent, onSelect, filter, attributeSupplier, valueSupplier,
 853                                 TypeLabelProvider::getColorOrDefault, hasBorder ? SWT.BORDER : SWT.NONE);
 854 
 855                 MenuManager addFromSelectionPredicate = new MenuManager(Messages.FILTER_ADD_FROM_SELECTION);
 856                 editor.getContextMenu().prependToGroup(MCContextMenuManager.GROUP_NEW, addFromSelectionPredicate);
 857                 addFromSelectionPredicate.setRemoveAllWhenShown(true);
 858                 addFromSelectionPredicate.addMenuListener(new IMenuListener() {
 859 
 860                         @Override
 861                         public void menuAboutToShow(IMenuManager manager) {
 862                                 selections.get().forEach(entry -> {
 863                                         MenuManager selectionFlavors = new MenuManager(entry.getName());
 864                                         entry.getSelection().getFlavors(editor.getFilter(), items, null)
 865                                                         .filter(f -> f instanceof IFilterFlavor).forEach(flavor -> {
 866                                                                 selectionFlavors.add(new Action(flavor.getName()) {
 867                                                                         @Override
 868                                                                         public void run() {
 869                                                                                 editor.addRoot(((IFilterFlavor) flavor).getFilter());
 870                                                                         }
 871                                                                 });
 872                                                         });
 873                                         if (!selectionFlavors.isEmpty()) {
 874                                                 if (manager.find(Messages.FILTER_NO_SELECTION_AVAILABLE) != null) {
 875                                                         manager.remove(Messages.FILTER_NO_SELECTION_AVAILABLE);
 876                                                 }
 877                                                 manager.add(selectionFlavors);
 878                                         } else {
 879                                                 manager.add(disabledAction(Messages.FILTER_NO_SELECTION_AVAILABLE));
 880                                         }
 881                                 });
 882                         }
 883                 });
 884 
 885                 // FIXME: This could potentially move into the FilterEditor class
 886                 MenuManager addAttributeValuePredicate = new MenuManager(Messages.FILTER_ADD_FROM_ATTRIBUTE);
 887                 editor.getContextMenu().prependToGroup(MCContextMenuManager.GROUP_NEW, addAttributeValuePredicate);
 888                 addAttributeValuePredicate.setRemoveAllWhenShown(true);
 889                 addAttributeValuePredicate.addMenuListener(new IMenuListener() {
 890                         Collection<IAttribute<?>> attributes;
 891 
 892                         @Override
 893                         public void menuAboutToShow(IMenuManager manager) {
 894                                 if (attributes == null) {
 895                                         attributes = attributeSupplier.get();
 896                                 }
 897                                 if (!attributes.isEmpty()) {
 898                                         if (manager.find(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE) != null) {
 899                                                 manager.remove(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE);
 900                                         }
 901                                         attributes.stream().distinct().sorted((a1, a2) -> a1.getName().compareTo(a2.getName()))
 902                                                 .forEach(attr -> {
 903                                                         addAttributeValuePredicate.add(new Action(attr.getName()) {
 904                                                                 @Override
 905                                                                 public void run() {
 906                                                                         IItemFilter filter = createDefaultFilter(items, attr);
 907                                                                         editor.addRoot(filter);
 908                                                                 }
 909                                                         });
 910                                                 });
 911                                 } else {
 912                                         manager.add(disabledAction(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE));
 913                                 }
 914 
 915                         }
 916                 });
 917                 return editor;
 918         }
 919 
 920         // FIXME: Move to some AttributeToolkit?
 921         private static Stream<IAttribute<?>> getAttributes(IItemCollection items) {
 922                 return ItemCollectionToolkit.stream(items).filter(IItemIterable::hasItems)
 923                                 .flatMap(is -> is.getType().getAttributes().stream());
 924         }
 925 
 926         public static Stream<IAttribute<?>> getPersistableAttributes(Stream<IAttribute<?>> attributes) {
 927                 // FIXME: Would like to always be able to persist a string representation of the attribute, because this is usable by filters.
 928 
 929                 // FIXME: Should we always include event type? Does it make any sense, except on the custom pages?
 930 
 931                 // FIXME: Transform both START_TIME and END_TIME to LIFETIME?
 932                 // FIXME: Add derived attributes, like a conversion of any THREAD or CLASS attribute? Thread group?
 933                 /*
 934                  * Make sure to do the conversions in the right order, so for example a stack trace can be
 935                  * converted to a top method, which then is converted to a method string.
 936                  */
 937                 return attributes.map(a -> a.equals(JfrAttributes.EVENT_THREAD) ? JdkAttributes.EVENT_THREAD_NAME : a)
 938                                 .flatMap(a -> a.equals(JfrAttributes.EVENT_STACKTRACE) ? Stream.of(JdkAttributes.STACK_TRACE_STRING,
 939                                                 JdkAttributes.STACK_TRACE_TOP_METHOD_STRING, JdkAttributes.STACK_TRACE_TOP_CLASS_STRING,
 940                                                 JdkAttributes.STACK_TRACE_TOP_PACKAGE, JdkAttributes.STACK_TRACE_BOTTOM_METHOD_STRING)
 941                                                 : Stream.of(a))
 942                                 .map(a -> a.equals(JdkAttributes.COMPILER_METHOD) ? JdkAttributes.COMPILER_METHOD_STRING : a)
 943                                 // FIXME: String or id?
 944                                 .map(a -> a.equals(JdkAttributes.REC_SETTING_FOR) ? JdkAttributes.REC_SETTING_FOR_NAME : a)
 945                                 .map(a -> a.equals(JdkAttributes.CLASS_DEFINING_CLASSLOADER)
 946                                                 ? JdkAttributes.CLASS_DEFINING_CLASSLOADER_STRING : a)
 947                                 .map(a -> a.equals(JdkAttributes.CLASS_INITIATING_CLASSLOADER)
 948                                                 ? JdkAttributes.CLASS_INITIATING_CLASSLOADER_STRING : a)
 949                                 .map(a -> a.equals(JdkAttributes.PARENT_CLASSLOADER)
 950                                                 ? JdkAttributes.PARENT_CLASSLOADER_STRING : a)
 951                                 .map(a -> a.equals(JdkAttributes.CLASSLOADER)
 952                                                 ? JdkAttributes.CLASSLOADER_STRING : a)
 953                                 .filter(a -> a.equals(JfrAttributes.EVENT_TYPE) || (a.getContentType() instanceof RangeContentType)
 954                                                 || (a.getContentType().getPersister() != null))
 955                                 .distinct();
 956         }
 957 
 958         /**
 959          * Returns a value for attribute, firstly by trying to find one in the items, secondly by
 960          * creating a default value for some known content types. Returns null if the first two cases
 961          * fail.
 962          *
 963          * @param items
 964          * @param attribute
 965          * @return a value of type V, or null
 966          */
 967         @SuppressWarnings("unchecked")
 968         private static <V> V findValueForFilter(IItemCollection items, ICanonicalAccessorFactory<V> attribute) {
 969                 IItem firstItem = ItemCollectionToolkit.stream(items).filter(is -> is.getType().hasAttribute(attribute))
 970                                 .flatMap(ItemIterableToolkit::stream)
 971                                 .filter(i -> ((IMemberAccessor<V, IItem>) attribute.getAccessor(i.getType())).getMember(i) != null)
 972                                 .findFirst().orElse(null);
 973                 if (firstItem != null) {
 974                         IMemberAccessor<V, IItem> accessor = (IMemberAccessor<V, IItem>) attribute.getAccessor(firstItem.getType());
 975                         return accessor.getMember(firstItem);
 976                 }
 977                 if (UnitLookup.PLAIN_TEXT.equals(attribute.getContentType())) {
 978                         return (V) ""; //$NON-NLS-1$
 979                 }
 980                 if (attribute.getContentType() instanceof KindOfQuantity<?>) {
 981                         return (V) ((KindOfQuantity<?>) attribute.getContentType()).getDefaultUnit().quantity(0);
 982                 }
 983                 return null;
 984         }
 985 
 986         /**
 987          * Returns an default filter for attribute, which might be an equals filter, hasAttribute
 988          * filter, or type filter, depending on the attribute and the contents of the items.
 989          *
 990          * @param items
 991          * @param attribute
 992          * @return a filter
 993          */
 994         // FIXME: Should move to FilterEditor, or some subclass/specialization?
 995         private static <V> IItemFilter createDefaultFilter(IItemCollection items, ICanonicalAccessorFactory<V> attribute) {
 996                 V value = findValueForFilter(items, attribute);
 997                 if (value == null) {
 998                         return ItemFilters.hasAttribute(attribute);
 999                 } else if (attribute.equals(JfrAttributes.EVENT_TYPE)) {
1000                         return ItemFilters.type(((IType<?>) value).getIdentifier());
1001                 }
1002                 return ItemFilters.equals(attribute, value);
1003         }
1004 
1005         public static void addRenameAction(Form form, IPageContainer editor) {
1006                 form.getMenuManager().add(new Action(Messages.PAGE_RENAME_MENU_ACTION) {
1007                         @Override
1008                         public void run() {
1009                                 InputDialog dialog = new InputDialog(form.getShell(), Messages.PAGE_RENAME_DIALOG_TITLE,
1010                                                 Messages.PAGE_RENAME_DIALOG_MESSAGE, form.getText(), null);
1011                                 if (dialog.open() == Window.OK) {
1012                                         form.setText(dialog.getValue());
1013                                         editor.currentPageRefresh();
1014                                 }
1015                         }
1016                 });
1017         }
1018 
1019         public static void addIconChangeAction(Form form, IPageContainer editor, Consumer<Image> newIconConsumer) {
1020                 form.getMenuManager().add(new Action(Messages.PAGE_CHANGE_ICON_MENU_ACTION) {
1021                         @Override
1022                         public void run() {
1023                                 WizardDialog dialog = new WizardDialog(form.getShell(),
1024                                                 new IconChangeWizard(form.getImage(), newIconConsumer));
1025                                 dialog.open();
1026                                 editor.currentPageRefresh();
1027                         }
1028                 });
1029         }
1030 
1031         private static class IconChangeWizard extends Wizard {
1032 
1033                 private final Image currentImage;
1034                 private final Consumer<Image> imageConsumer;
1035                 private Label imageLabel;
1036 
1037                 public IconChangeWizard(Image currentImage, Consumer<Image> imageConsumer) {
1038                         setWindowTitle(Messages.PAGE_CHANGE_ICON_WIZARD_TITLE);
1039                         this.currentImage = currentImage;
1040                         this.imageConsumer = imageConsumer;
1041                 }
1042 
1043                 @Override
1044                 public void addPages() {
1045                         addPage(new WizardPage(Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_TITLE) {
1046 
1047                                 @Override
1048                                 public String getTitle() {
1049                                         return Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_TITLE;
1050                                 }
1051 
1052                                 @Override
1053                                 public String getDescription() {
1054                                         return Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_DESC;
1055                                 }
1056 
1057                                 @Override
1058                                 public void createControl(Composite parent) {
1059                                         Composite container = new Composite(parent, SWT.NONE);
1060                                         GridLayout layout = new GridLayout(1, false);
1061                                         container.setLayout(layout);
1062 
1063                                         Button button = new Button(container, SWT.NONE);
1064                                         button.setText(Messages.PAGE_CHANGE_ICON_CHOOSE_IMAGE_FILE);
1065 
1066                                         button.addSelectionListener(new SelectionAdapter() {
1067                                                 @Override
1068                                                 public void widgetSelected(SelectionEvent e) {
1069                                                         chooseImageFileDialog();
1070                                                 }
1071                                         });
1072 
1073                                         if (currentImage != null) {
1074                                                 new Label(container, SWT.NONE).setText(Messages.PAGE_CHANGE_ICON_CURRENT_ICON);
1075                                                 new Label(container, SWT.BORDER).setImage(currentImage);
1076                                         }
1077                                         new Label(container, SWT.NONE).setText(Messages.PAGE_CHANGE_ICON_NEW_ICON_PREVIEW);
1078                                         imageLabel = new Label(container, SWT.BORDER);
1079                                         GridData gd = new GridData(16, 16);
1080                                         imageLabel.setLayoutData(gd);
1081 
1082                                         setControl(container);
1083                                 }
1084 
1085                                 private void chooseImageFileDialog() {
1086                                         FileDialog fileDialog = new FileDialog(getShell(), SWT.OPEN);
1087                                         String[] filterNames = new String[] {"Image Files", "All Files (*)"}; //$NON-NLS-1$ //$NON-NLS-2$
1088                                         String[] filterExtensions = new String[] {"*.gif;*.png;*.xpm;*.jpg;*.jpeg;*.tiff", "*"}; //$NON-NLS-1$ //$NON-NLS-2$
1089                                         fileDialog.setFilterNames(filterNames);
1090                                         fileDialog.setFilterExtensions(filterExtensions);
1091                                         String filename = fileDialog.open();
1092                                         if (filename == null) {
1093                                                 // Dialog was cancelled. Bail out early to avoid handling that case later. Premature?
1094                                                 return;
1095                                         }
1096                                         try (InputStream fis = new FileInputStream(filename)) {
1097                                                 ImageData imageData = new ImageData(fis);
1098                                                 // Validate image data
1099                                                 if (imageData.width != 16 || imageData.height != 16) {
1100                                                         imageData = resizeImage(imageData, 16, 16);
1101                                                 }
1102                                                 DisplayToolkit.dispose(imageLabel.getImage());
1103                                                 imageLabel.setImage(new Image(getShell().getDisplay(), imageData));
1104                                                 imageLabel.getParent().layout();
1105                                                 setPageComplete(isPageComplete());
1106                                         } catch (Exception e) {
1107                                                 // FIXME: Add proper logging
1108                                                 e.printStackTrace();
1109                                         }
1110                                 }
1111 
1112                                 private ImageData resizeImage(ImageData imageData, int width, int height) {
1113                                         Image original = ImageDescriptor.createFromImageData(imageData).createImage();
1114                                         Image scaled = new Image(Display.getDefault(), width, height);
1115                                         GC gc = new GC(scaled);
1116                                         gc.setAntialias(SWT.ON);
1117                                         gc.setInterpolation(SWT.HIGH);
1118                                         gc.drawImage(original, 0, 0, imageData.width, imageData.height, 0, 0, width, height);
1119                                         gc.dispose();
1120                                         original.dispose();
1121                                         ImageData scaledData = scaled.getImageData();
1122                                         scaled.dispose();
1123                                         return scaledData;
1124                                 }
1125 
1126                                 @Override
1127                                 public boolean isPageComplete() {
1128                                         return imageLabel.getImage() != null;
1129                                 }
1130 
1131                         });
1132                 }
1133 
1134                 @Override
1135                 public boolean performFinish() {
1136                         imageConsumer.accept(imageLabel.getImage());
1137                         DisplayToolkit.dispose(currentImage);
1138                         return true;
1139                 }
1140 
1141                 @Override
1142                 public boolean performCancel() {
1143                         DisplayToolkit.dispose(imageLabel.getImage());
1144                         return true;
1145                 }
1146 
1147         }
1148 
1149         public static ItemList createSimpleItemList(
1150                 Composite parent, ItemListBuilder listBuilder, IPageContainer pageContainer, TableSettings tableSettings,
1151                 String selectionName) {
1152 
1153                 ItemList list = listBuilder.build(parent, tableSettings);
1154                 ColumnViewer viewer = list.getManager().getViewer();
1155                 MCContextMenuManager mm = MCContextMenuManager.create(viewer.getControl());
1156                 ColumnMenusFactory.addDefaultMenus(list.getManager(), mm);
1157                 viewer.addSelectionChangedListener(
1158                                 e -> pageContainer.showSelection(ItemCollectionToolkit.build(list.getSelection().get())));
1159 
1160                 if (selectionName != null) {
1161                         SelectionStoreActionToolkit.addSelectionStoreActions(pageContainer.getSelectionStore(), list, selectionName,
1162                                         mm);
1163                 }
1164 
1165                 return list;
1166         }
1167 
1168         public static void addTabItem(CTabFolder tabFolder, Control section, String name) {
1169                 CTabItem tabItem = new CTabItem(tabFolder, SWT.NONE);
1170                 tabItem.setControl(section);
1171                 tabItem.setText(name);
1172         }
1173 
1174         public static TypeFilterBuilder buildEventTypeTree(
1175                 Composite parent, FormToolkit toolkit, Runnable onChange, boolean checkbox) {
1176                 // TODO: Make more accessible.
1177                 // TODO: Add support for storing the expansion state in a memento.
1178                 // TODO: Add input from selection store, output to selection store
1179                 // TODO: Add toolbar for choosing tree or checkbox tree.
1180                 Composite treeComposite = new Composite(parent, SWT.NONE);
1181                 treeComposite.setLayout(new GridLayout());
1182                 toolkit.adapt(treeComposite);
1183                 Label caption = toolkit.createLabel(treeComposite, Messages.EVENT_TYPE_TREE_TITLE);
1184                 caption.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
1185                 caption.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
1186 
1187                 TypeFilterBuilder typeFilterTree = new TypeFilterBuilder(treeComposite, onChange, checkbox);
1188 
1189                 typeFilterTree.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
1190                 return typeFilterTree;
1191         }
1192 
1193         public static boolean isTypeWithThreadAndDuration(IType<?> type) {
1194                 return JfrAttributes.EVENT_THREAD.getAccessor(type) != null
1195                                 && JfrAttributes.START_TIME.getAccessor(type) != JfrAttributes.END_TIME.getAccessor(type);
1196         }
1197 
1198         public static void addPage(Set<IType<?>> selectedTypes) {
1199                 PageManager pm = FlightRecorderUI.getDefault().getPageManager();
1200                 pm.makeRoot(pm.createPage(ItemHandlerPage.Factory.class, new ItemHandlerUiStandIn(selectedTypes)));
1201         }
1202 }