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.util.ArrayList; 38 import java.util.Collection; 39 import java.util.Collections; 40 import java.util.List; 41 import java.util.stream.Collectors; 42 import java.util.stream.Stream; 43 44 import org.eclipse.jface.action.IAction; 45 import org.eclipse.jface.layout.GridDataFactory; 46 import org.eclipse.jface.layout.GridLayoutFactory; 47 import org.eclipse.jface.viewers.ArrayContentProvider; 48 import org.eclipse.jface.viewers.CellEditor; 49 import org.eclipse.jface.viewers.CheckboxTableViewer; 50 import org.eclipse.jface.viewers.ColumnLabelProvider; 51 import org.eclipse.jface.viewers.EditingSupport; 52 import org.eclipse.jface.viewers.IStructuredSelection; 53 import org.eclipse.jface.viewers.StructuredSelection; 54 import org.eclipse.jface.viewers.TableViewerColumn; 55 import org.eclipse.jface.viewers.TextCellEditor; 56 import org.eclipse.jface.viewers.ViewerCell; 57 import org.eclipse.jface.window.ToolTip; 58 import org.eclipse.jface.window.Window; 59 import org.eclipse.jface.wizard.WizardPage; 60 import org.eclipse.osgi.util.NLS; 61 import org.eclipse.swt.SWT; 62 import org.eclipse.swt.graphics.Point; 63 import org.eclipse.swt.widgets.Composite; 64 import org.eclipse.swt.widgets.Control; 65 import org.eclipse.swt.widgets.Event; 66 import org.eclipse.swt.widgets.Label; 67 import org.eclipse.ui.IWorkbenchCommandConstants; 68 import org.eclipse.ui.forms.widgets.FormText; 69 70 import org.openjdk.jmc.common.IDescribable; 71 import org.openjdk.jmc.common.IPredicate; 72 import org.openjdk.jmc.common.IState; 73 import org.openjdk.jmc.common.IStateful; 74 import org.openjdk.jmc.common.IWritableState; 75 import org.openjdk.jmc.common.item.IItem; 76 import org.openjdk.jmc.common.item.IItemFilter; 77 import org.openjdk.jmc.common.item.IType; 78 import org.openjdk.jmc.common.item.ItemFilters; 79 import org.openjdk.jmc.common.item.ItemFilters.Types; 80 import org.openjdk.jmc.common.item.PersistableItemFilter; 81 import org.openjdk.jmc.common.util.PredicateToolkit; 82 import org.openjdk.jmc.common.util.StateToolkit; 83 import org.openjdk.jmc.flightrecorder.ui.EventTypeFolderNode; 84 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages; 85 import org.openjdk.jmc.ui.UIPlugin; 86 import org.openjdk.jmc.ui.handlers.ActionToolkit; 87 import org.openjdk.jmc.ui.handlers.MCContextMenuManager; 88 import org.openjdk.jmc.ui.misc.ActionUiToolkit; 89 import org.openjdk.jmc.ui.misc.CompositeToolkit; 90 import org.openjdk.jmc.ui.misc.DialogToolkit; 91 import org.openjdk.jmc.ui.misc.SWTColorToolkit; 92 import org.openjdk.jmc.ui.wizards.IPerformFinishable; 93 import org.openjdk.jmc.ui.wizards.OnePageWizardDialog; 94 95 public class LaneEditor { 96 97 private static final IItemFilter TYPE_HAS_THREAD_AND_DURATION = new IItemFilter() { 98 @Override 99 public IPredicate<IItem> getPredicate(IType<IItem> type) { 100 if (DataPageToolkit.isTypeWithThreadAndDuration(type)) { 101 return PredicateToolkit.truePredicate(); 102 } 103 return PredicateToolkit.falsePredicate(); 104 } 105 }; 106 107 private static class EditLanesWizardPage extends WizardPage implements IPerformFinishable { 108 109 private final EventTypeFolderNode root; 110 private final List<LaneDefinition> lanes; 111 private LaneDefinition restLane; 112 private TypeFilterBuilder filterEditor; 113 private CheckboxTableViewer lanesViewer; 114 private Object selected; 115 116 private EditLanesWizardPage(EventTypeFolderNode root, Collection<LaneDefinition> lanesInput) { 117 super("EditFilterLanesPage"); //$NON-NLS-1$ 118 this.root = root; 119 this.lanes = new ArrayList<>(lanesInput); 120 restLane = ensureRestLane(lanes); 121 } 122 123 @Override 124 public void createControl(Composite parent) { 125 // FIXME: Do we want to group under categories somehow, or just hide the filters that don't have any existing event types. 126 Composite container = new Composite(parent, SWT.NONE); 127 container.setLayout(GridLayoutFactory.swtDefaults().numColumns(2).create()); 128 129 Composite laneHeaderContainer = new Composite(container, SWT.NONE); 130 laneHeaderContainer.setLayout(GridLayoutFactory.swtDefaults().create()); 131 laneHeaderContainer.setLayoutData(GridDataFactory.fillDefaults().create()); 132 133 // FIXME: Add a duplicate action? 134 IAction moveUpAction = ActionToolkit.action(() -> moveSelected(true), Messages.LANES_MOVE_UP_ACTION, 135 UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_NAV_UP)); 136 IAction moveDownAction = ActionToolkit.action(() -> moveSelected(false), Messages.LANES_MOVE_DOWN_ACTION, 137 UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_NAV_DOWN)); 138 IAction addAction = ActionToolkit.action(this::addLane, Messages.LANES_ADD_LANE_ACTION, 139 UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_ADD)); 140 IAction removeAction = ActionToolkit.commandAction(this::deleteSelected, 141 IWorkbenchCommandConstants.EDIT_DELETE); 142 Control toolbar = ActionUiToolkit.buildToolBar(laneHeaderContainer, 143 Stream.of(moveUpAction, moveDownAction, addAction, removeAction), false); 144 toolbar.setLayoutData(GridDataFactory.fillDefaults().create()); 145 146 Label lanesTitle = new Label(laneHeaderContainer, SWT.NONE); 147 lanesTitle.setText(Messages.LANES_EDITOR_LABEL); 148 lanesTitle.setLayoutData(GridDataFactory.fillDefaults().create()); 149 Label filterTitle = new Label(container, SWT.NONE); 150 filterTitle.setText(Messages.LANES_FILTER_LABEL); 151 filterTitle.setLayoutData( 152 GridDataFactory.fillDefaults().grab(true, false).align(SWT.BEGINNING, SWT.END).create()); 153 154 lanesViewer = CheckboxTableViewer.newCheckList(container, SWT.BORDER | SWT.V_SCROLL); 155 TableViewerColumn viewerColumn = new TableViewerColumn(lanesViewer, SWT.NONE); 156 viewerColumn.getColumn().setText(Messages.LANES_LANE_COLUMN); 157 viewerColumn.getColumn().setWidth(200); 158 // FIXME: Would like to enable editing by some other means than single-clicking, but seems a bit tricky. 159 viewerColumn.setEditingSupport(new EditingSupport(lanesViewer) { 160 161 private String currentName; 162 163 @Override 164 protected void setValue(Object element, Object value) { 165 String newName = value.toString(); 166 if (currentName != null && currentName.equals(newName)) { 167 return; 168 } 169 LaneDefinition oldLd = (LaneDefinition) element; 170 LaneDefinition newLane = new LaneDefinition(value.toString(), oldLd.enabled, oldLd.filter, 171 oldLd.isRestLane); 172 int elementIndex = lanes.indexOf(element); 173 lanes.set(elementIndex, newLane); 174 lanesViewer.replace(newLane, elementIndex); 175 getViewer().update(element, null); 176 } 177 178 @Override 179 protected Object getValue(Object element) { 180 currentName = ((LaneDefinition) element).getName(); 181 return currentName; 182 } 183 184 @Override 185 protected CellEditor getCellEditor(Object element) { 186 return new TextCellEditor((Composite) getViewer().getControl()); 187 } 188 189 @Override 190 protected boolean canEdit(Object element) { 191 return true; 192 193 } 194 }); 195 196 lanesViewer.setLabelProvider(new ColumnLabelProvider() { 197 198 @Override 199 public String getText(Object element) { 200 if (element instanceof LaneDefinition) { 201 if (element == selected) { 202 return ((LaneDefinition) element).getNameOrCount(filterEditor.getCheckedTypeIds().count()); 203 } else { 204 return ((LaneDefinition) element).getName(); 205 } 206 } 207 return super.getText(element); 208 }; 209 210 // FIXME: Do we want to use italics for empty lanes? 211 // @Override 212 // public Font getFont(Object element) { 213 // if (getTypesCount(element) > 0) { 214 // return JFaceResources.getFontRegistry().get(JFaceResources.DEFAULT_FONT); 215 // } else { 216 // return JFaceResources.getFontRegistry().getItalic(JFaceResources.DEFAULT_FONT); 217 // } 218 // } 219 // 220 // private long getTypesCount(Object element) { 221 // if (element == selected) { 222 // return filterEditor.getCheckedTypeIds().count(); 223 // } else if (element instanceof LaneDefinition) { 224 // return ((LaneDefinition)element).getTypesCount(); 225 // } 226 // return 0; 227 // } 228 }); 229 lanesViewer.setContentProvider(ArrayContentProvider.getInstance()); 230 // FIXME: Can we potentially reuse this tooltip in the legend as well? 231 new ToolTip(lanesViewer.getControl(), ToolTip.NO_RECREATE, false) { 232 233 @Override 234 protected ViewerCell getToolTipArea(Event event) { 235 return lanesViewer.getCell(new Point(event.x, event.y)); 236 } 237 238 @Override 239 protected Composite createToolTipContentArea(Event event, Composite parent) { 240 FormText formText = CompositeToolkit.createInfoFormText(parent); 241 Object element = getToolTipArea(event).getElement(); 242 Stream<String> ids = Stream.empty(); 243 if (element == selected) { 244 ids = filterEditor.getCheckedTypeIds(); 245 } else if (element instanceof LaneDefinition 246 && ((LaneDefinition) element).filter instanceof Types) { 247 ids = ((Types) ((LaneDefinition) element).filter).getTypes().stream(); 248 } 249 StringBuilder sb = new StringBuilder(); 250 ids.forEach(typeId -> { 251 Color color = TypeLabelProvider.getColorOrDefault(typeId); 252 formText.setImage(typeId, SWTColorToolkit.getColorThumbnail(SWTColorToolkit.asRGB(color))); 253 sb.append("<li style='image' value='" + typeId + "'>" + typeId + "</li>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 254 }); 255 if (sb.length() > 0) { 256 sb.insert(0, "<form>"); //$NON-NLS-1$ 257 sb.append("</form>"); //$NON-NLS-1$ 258 formText.setText(sb.toString(), true, false); 259 } else { 260 formText.setText(Messages.LANES_CHECK_TO_INCLUDE, false, false); 261 } 262 return formText; 263 } 264 }; 265 lanesViewer.setInput(lanes); 266 lanesViewer.setCheckedElements(lanes.stream().filter(ld -> ld.isEnabled()).toArray()); 267 MCContextMenuManager mm = MCContextMenuManager.create(lanesViewer.getControl()); 268 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, moveUpAction); 269 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, moveDownAction); 270 // FIXME: Add icon 271 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, addAction); 272 273 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, removeAction); 274 275 filterEditor = new TypeFilterBuilder(container, this::onTypeFilterChange); 276 filterEditor.setInput(root); 277 filterEditor.getControl().setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create()); 278 lanesViewer.getControl().setLayoutData(GridDataFactory.fillDefaults().grab(false, true).create()); 279 280 lanesViewer.addSelectionChangedListener( 281 e -> laneSelectionChanges(((IStructuredSelection) e.getSelection()).getFirstElement())); 282 LaneDefinition firstLane = lanes.get(0); 283 lanesViewer.setSelection(new StructuredSelection(firstLane)); 284 285 setControl(container); 286 } 287 288 private void addLane() { 289 int selectIndex = Math.max(0, lanes.indexOf(selected)); 290 IItemFilter emptyFilter = ItemFilters.type(Collections.emptySet()); 291 LaneDefinition newEmpty = new LaneDefinition(null, false, emptyFilter, false); 292 lanes.add(selectIndex + 1, newEmpty); 293 lanesViewer.insert(newEmpty, selectIndex + 1); 294 lanesViewer.setSelection(new StructuredSelection(newEmpty)); 295 } 296 297 private void onTypeFilterChange() { 298 if (selected instanceof LaneDefinition) { 299 LaneDefinition selectedLane = (LaneDefinition) selected; 300 if (selectedLane.isRestLane()) { 301 DialogToolkit.showWarningDialogAsync(lanesViewer.getControl().getDisplay(), 302 Messages.LANES_EDIT_NOT_ALLOWED_WARNING, 303 NLS.bind(Messages.LANES_EDIT_NOT_ALLOWED_WARNING_DESC, selectedLane.getName())); 304 // FIXME: Can we refresh the filter editor to show that nothing has changed? 305 } 306 } 307 lanesViewer.update(selected, null); 308 } 309 310 private void deleteSelected() { 311 // FIXME: It's currently not possible to delete the last lane 312 int selectIndex = Math.max(0, lanes.indexOf(selected) - 1); 313 if (selected instanceof LaneDefinition && ((LaneDefinition) selected).isRestLane()) { 314 lanes.remove(selected); 315 lanesViewer.setSelection(new StructuredSelection(lanes.get(selectIndex))); 316 lanesViewer.refresh(); 317 } else { 318 DialogToolkit.showWarningDialogAsync(lanesViewer.getControl().getDisplay(), 319 Messages.LANES_DELETE_NOT_ALLOWED_WARNING, NLS.bind( 320 Messages.LANES_DELETE_NOT_ALLOWED_WARNING_DESC, ((LaneDefinition) selected).getName())); 321 } 322 } 323 324 private void moveSelected(boolean up) { 325 int fromIndex = lanes.indexOf(selected); 326 int toIndex = fromIndex + (up ? -1 : 1); 327 if (fromIndex >= 0 && toIndex >= 0 && toIndex < lanes.size()) { 328 LaneDefinition removed = lanes.remove(fromIndex); 329 lanes.add(toIndex, removed); 330 lanesViewer.refresh(); 331 } 332 } 333 334 private void laneSelectionChanges(Object newSelected) { 335 int selectedIndex = lanes.indexOf(newSelected); 336 if (this.selected != newSelected) { 337 saveFilter(); 338 this.selected = lanes.get(selectedIndex); 339 if (selected instanceof LaneDefinition) { 340 Types typesFilter; 341 if (((LaneDefinition) selected).getFilter() instanceof Types) { 342 typesFilter = ((Types) ((LaneDefinition) selected).getFilter()); 343 } else { 344 typesFilter = (Types) ItemFilters.convertToTypes(((LaneDefinition) selected).getFilter(), 345 filterEditor.getAllTypes()); 346 } 347 filterEditor.selectTypes(typesFilter.getTypes()); 348 } 349 } 350 } 351 352 private void saveFilter() { 353 int selectedIndex = lanes.indexOf(selected); 354 if (selectedIndex >= 0) { 355 LaneDefinition ld = lanes.get(selectedIndex); 356 if (!ld.isRestLane()) { 357 IItemFilter newFilter = ItemFilters 358 .type(filterEditor.getCheckedTypeIds().collect(Collectors.toSet())); 359 LaneDefinition newLd = new LaneDefinition(ld.name, lanesViewer.getChecked(ld), newFilter, 360 ld.isRestLane); 361 lanes.set(selectedIndex, newLd); 362 lanesViewer.replace(newLd, selectedIndex); 363 if (restLane != null) { 364 LaneDefinition newRest = new LaneDefinition(restLane.name, restLane.enabled, 365 getRestFilter(lanes), true); 366 int restIndex = lanes.indexOf(restLane); 367 lanes.set(restIndex, newRest); 368 lanesViewer.replace(newRest, restIndex); 369 restLane = newRest; 370 } 371 lanesViewer.refresh(); 372 } 373 } 374 } 375 376 @Override 377 public boolean performFinish() { 378 saveFilter(); 379 for (int i = 0; i < lanes.size(); i++) { 380 LaneDefinition ld = lanes.get(i); 381 if (ld.isEnabled() != lanesViewer.getChecked(ld)) { 382 lanes.set(i, new LaneDefinition(ld.name, lanesViewer.getChecked(ld), ld.filter, ld.isRestLane)); 383 } 384 } 385 return true; 386 } 387 } 388 389 public static class LaneDefinition implements IDescribable, IStateful { 390 391 private static final String FILTER = "filter"; //$NON-NLS-1$ 392 private static final String NAME = "name"; //$NON-NLS-1$ 393 private static final String ENABLED = "enabled"; //$NON-NLS-1$ 394 private static final String IS_REST_LANE = "isRestLane"; //$NON-NLS-1$ 395 396 private final String name; 397 private final IItemFilter filter; 398 private final boolean enabled; 399 private final boolean isRestLane; 400 401 public LaneDefinition(String name, boolean enabled, IItemFilter filter, boolean isRestLane) { 402 this.name = name; 403 this.enabled = enabled; 404 this.filter = filter; 405 this.isRestLane = isRestLane; 406 } 407 408 @Override 409 public String getName() { 410 long count = filter instanceof Types ? ((Types) filter).getTypes().size() : 0; 411 return getNameOrCount(count); 412 } 413 414 public String getNameOrCount(long count) { 415 return name != null ? name 416 : count == 1 && ((Types) filter).getTypes().iterator().hasNext() 417 ? ((Types) filter).getTypes().iterator().next() 418 : count > 0 ? NLS.bind(Messages.LANES_DEFINITION_NAME, count) : Messages.LANES_EMPTY_LANE; 419 } 420 421 @Override 422 public String getDescription() { 423 return NLS.bind(Messages.LANES_DEFINITION_DESC, getName()); 424 } 425 426 public IItemFilter getFilter() { 427 return filter; 428 } 429 430 public boolean isEnabled() { 431 return enabled; 432 } 433 434 public boolean isRestLane() { 435 return isRestLane; 436 } 437 438 public boolean isEnabledAndNotRestLane() { 439 return enabled && !isRestLane; 440 } 441 442 @Override 443 public void saveTo(IWritableState writableState) { 444 writableState.putString(NAME, name); 445 StateToolkit.writeBoolean(writableState, ENABLED, enabled); 446 StateToolkit.writeBoolean(writableState, IS_REST_LANE, isRestLane); 447 if (!isRestLane && filter != null) { 448 ((PersistableItemFilter) filter).saveTo(writableState.createChild(FILTER)); 449 } 450 } 451 452 public static LaneDefinition readFrom(IState memento) { 453 String name = memento.getAttribute(NAME); 454 boolean enabled = StateToolkit.readBoolean(memento, ENABLED, false); 455 boolean isRestLane = StateToolkit.readBoolean(memento, IS_REST_LANE, false); 456 IState filterState = memento.getChild(FILTER); 457 IItemFilter filter; 458 if (isRestLane) { 459 filter = null; 460 } else if (filterState != null) { 461 filter = PersistableItemFilter.readFrom(filterState); 462 } else { 463 throw new UnsupportedOperationException("Null filter not allowed for thread lane: " + name); //$NON-NLS-1$ 464 } 465 // FIXME: Should probably warn if filter is not an instance of Types, and possibly handle other type filter variants as well, like TypeMatches. 466 return new LaneDefinition(name, enabled, filter, isRestLane); 467 } 468 469 @Override 470 public String toString() { 471 return getName() + "(" + enabled + ")"; //$NON-NLS-1$ //$NON-NLS-2$ 472 } 473 } 474 475 public static List<LaneDefinition> openDialog( 476 EventTypeFolderNode root, List<LaneDefinition> lanes, String title, String description) { 477 EditLanesWizardPage page = new EditLanesWizardPage(root, lanes); 478 page.setTitle(title); 479 page.setDescription(description); 480 if (OnePageWizardDialog.open(page, 500, 600) == Window.OK) { 481 return page.lanes.stream().filter(LaneEditor::laneIncludesTypes).collect(Collectors.toList()); 482 } 483 return lanes; 484 } 485 486 private static boolean laneIncludesTypes(LaneDefinition ld) { 487 return ld.isRestLane() || ld.getFilter() instanceof Types && ((Types) ld.getFilter()).getTypes().size() > 0; 488 } 489 490 private static IItemFilter getRestFilter(List<LaneDefinition> lanesInput) { 491 List<IItemFilter> laneFilters = lanesInput.stream().filter(ld -> !ld.isRestLane).map(ld -> ld.getFilter()) 492 .collect(Collectors.toList()); 493 IItemFilter laneFilter = ItemFilters.or(laneFilters.toArray(new IItemFilter[laneFilters.size()])); 494 return ItemFilters.and(ItemFilters.not(laneFilter), TYPE_HAS_THREAD_AND_DURATION); 495 } 496 497 public static LaneDefinition ensureRestLane(List<LaneDefinition> lanesInput) { 498 // FIXME: Should we react if there are several rest lanes specified, or just ignore the other ones? 499 LaneDefinition oldRestLane = lanesInput.stream().filter(ld -> ld.isRestLane).findAny().orElse(null); 500 LaneDefinition newRestLane; 501 IItemFilter restFilter = getRestFilter(lanesInput); 502 if (oldRestLane == null) { 503 newRestLane = new LaneDefinition(Messages.LANES_OTHER_TYPES, false, restFilter, true); 504 lanesInput.add(newRestLane); 505 } else { 506 newRestLane = new LaneDefinition(oldRestLane.name, oldRestLane.enabled, restFilter, true); 507 lanesInput.set(lanesInput.indexOf(oldRestLane), newRestLane); 508 } 509 return newRestLane; 510 } 511 }