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