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; 34 35 import static org.eclipse.ui.IWorkbenchCommandConstants.EDIT_COPY; 36 import static org.eclipse.ui.IWorkbenchCommandConstants.EDIT_DELETE; 37 import static org.eclipse.ui.IWorkbenchCommandConstants.EDIT_PASTE; 38 39 import java.util.List; 40 41 import org.eclipse.jface.action.Action; 42 import org.eclipse.jface.action.IAction; 43 import org.eclipse.jface.action.IMenuManager; 44 import org.eclipse.jface.action.MenuManager; 45 import org.eclipse.jface.dialogs.MessageDialog; 46 import org.eclipse.jface.resource.ImageDescriptor; 47 import org.eclipse.jface.util.IPropertyChangeListener; 48 import org.eclipse.jface.util.LocalSelectionTransfer; 49 import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; 50 import org.eclipse.jface.viewers.ISelection; 51 import org.eclipse.jface.viewers.IStructuredSelection; 52 import org.eclipse.jface.viewers.ITreeContentProvider; 53 import org.eclipse.jface.viewers.StructuredSelection; 54 import org.eclipse.jface.viewers.TreeViewer; 55 import org.eclipse.jface.viewers.Viewer; 56 import org.eclipse.jface.viewers.ViewerDropAdapter; 57 import org.eclipse.swt.SWT; 58 import org.eclipse.swt.dnd.DND; 59 import org.eclipse.swt.dnd.DragSourceAdapter; 60 import org.eclipse.swt.dnd.DragSourceEvent; 61 import org.eclipse.swt.dnd.Transfer; 62 import org.eclipse.swt.dnd.TransferData; 63 import org.eclipse.swt.graphics.Image; 64 import org.eclipse.swt.widgets.Composite; 65 import org.eclipse.swt.widgets.Display; 66 import org.eclipse.ui.IActionBars; 67 import org.eclipse.ui.PlatformUI; 68 import org.eclipse.ui.part.IPageSite; 69 import org.eclipse.ui.views.contentoutline.ContentOutlinePage; 70 71 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages; 72 import org.openjdk.jmc.flightrecorder.ui.pages.itemhandler.ItemHandlerPage; 73 import org.openjdk.jmc.flightrecorder.ui.pages.itemhandler.ItemHandlerPage.ItemHandlerUiStandIn; 74 import org.openjdk.jmc.flightrecorder.ui.preferences.PreferenceKeys; 75 import org.openjdk.jmc.ui.UIPlugin; 76 import org.openjdk.jmc.ui.handlers.ActionToolkit; 77 import org.openjdk.jmc.ui.handlers.MCContextMenuManager; 78 import org.openjdk.jmc.ui.misc.AdaptingLabelProvider; 79 import org.openjdk.jmc.ui.misc.ClipboardManager; 80 import org.openjdk.jmc.ui.misc.DisplayToolkit; 81 82 /** 83 * Outline page for the JFR editor. Tightly coupled with {@link AbstractJfrEditor}. 84 */ 85 public class JfrOutlinePage extends ContentOutlinePage { 86 87 private static final String HELP_CONTEXT_ID = FlightRecorderUI.PLUGIN_ID + ".JfrOutlinePage"; //$NON-NLS-1$ 88 static final ITreeContentProvider CONTENT_PROVIDER = new ITreeContentProvider() { 89 90 @Override 91 public void dispose() { 92 } 93 94 @Override 95 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { 96 } 97 98 @SuppressWarnings("unchecked") 99 @Override 100 public Object[] getElements(Object inputElement) { 101 return ((List<DataPageDescriptor>) inputElement).toArray(); 102 } 103 104 @Override 105 public Object[] getChildren(Object parentElement) { 106 return ((DataPageDescriptor) parentElement).getChildren(); 107 } 108 109 @Override 110 public Object getParent(Object element) { 111 return ((DataPageDescriptor) element).getParent(); 112 } 113 114 @Override 115 public boolean hasChildren(Object element) { 116 return ((DataPageDescriptor) element).hasChildren(); 117 } 118 119 }; 120 121 private final class OutlineDragListener extends DragSourceAdapter { 122 123 @Override 124 public void dragStart(DragSourceEvent event) { 125 if (PAGE_STRUCTURE_LOCK_ACTION.isChecked()) { 126 event.doit = false; 127 } else { 128 LocalSelectionTransfer.getTransfer().setSelection(getTreeViewer().getSelection()); 129 } 130 } 131 132 @Override 133 public void dragSetData(DragSourceEvent event) { 134 if (LocalSelectionTransfer.getTransfer().isSupportedType(event.dataType)) { 135 event.data = LocalSelectionTransfer.getTransfer().getSelection(); 136 } 137 } 138 139 @Override 140 public void dragFinished(DragSourceEvent event) { 141 LocalSelectionTransfer.getTransfer().setSelection(null); 142 } 143 } 144 145 private static final class OutlineDropListener extends ViewerDropAdapter { 146 147 public OutlineDropListener(TreeViewer viewer) { 148 super(viewer); 149 } 150 151 @Override 152 public boolean performDrop(Object data) { 153 if (data instanceof IStructuredSelection) { 154 Object selected = ((IStructuredSelection) data).getFirstElement(); 155 Object target = getCurrentTarget(); 156 if (selected instanceof DataPageDescriptor 157 && (target instanceof DataPageDescriptor || target == null)) { 158 DataPageDescriptor sDPD = (DataPageDescriptor) selected; 159 DataPageDescriptor tDPD = (DataPageDescriptor) target; 160 switch (getCurrentOperation()) { 161 case DND.DROP_COPY: 162 sDPD = new DataPageDescriptor(sDPD); 163 // Fall through 164 case DND.DROP_MOVE: 165 switch (getCurrentLocation()) { 166 case LOCATION_AFTER: 167 FlightRecorderUI.getDefault().getPageManager().makeSibling(sDPD, tDPD, 1); 168 return true; 169 case LOCATION_BEFORE: 170 FlightRecorderUI.getDefault().getPageManager().makeSibling(sDPD, tDPD, 0); 171 return true; 172 case LOCATION_ON: 173 FlightRecorderUI.getDefault().getPageManager().makeChild(sDPD, tDPD, 0); 174 return true; 175 case LOCATION_NONE: 176 FlightRecorderUI.getDefault().getPageManager().makeRoot(sDPD); 177 return true; 178 } 179 } 180 } 181 } 182 return false; 183 } 184 185 @Override 186 public boolean validateDrop(Object target, int operation, TransferData transferType) { 187 ISelection s = LocalSelectionTransfer.getTransfer().getSelection(); 188 if (s instanceof IStructuredSelection && !PAGE_STRUCTURE_LOCK_ACTION.isChecked()) { 189 Object selected = ((IStructuredSelection) s).getFirstElement(); 190 if (selected instanceof DataPageDescriptor) { 191 boolean cyclic = target instanceof DataPageDescriptor 192 && ((DataPageDescriptor) selected).contains((DataPageDescriptor) target); 193 return !cyclic; 194 } 195 } 196 return false; 197 } 198 } 199 200 private class OutlineLabelProvider extends AdaptingLabelProvider { 201 202 @Override 203 public String getText(Object element) { 204 if (element instanceof DataPageDescriptor) { 205 return editor.getDisplayablePage((DataPageDescriptor) element).getName(); 206 } 207 return super.getText(element); 208 } 209 210 @Override 211 public String getToolTipText(Object element) { 212 if (element instanceof DataPageDescriptor) { 213 if (FlightRecorderUI.getDefault().isAnalysisEnabled()) { 214 return editor.getDisplayablePage((DataPageDescriptor) element).getDescription(); 215 } else { 216 return null; 217 } 218 } 219 return super.getToolTipText(element); 220 } 221 222 @Override 223 public Image getImage(Object element) { 224 if (element instanceof DataPageDescriptor) { 225 ImageDescriptor imageDescriptor = editor.getDisplayablePage((DataPageDescriptor) element) 226 .getImageDescriptor(); 227 return imageDescriptor == null ? null : (Image) getResourceManager().get(imageDescriptor); 228 } 229 return super.getImage(element); 230 } 231 } 232 233 private final class NewPageAction extends Action { 234 235 private final DataPageDescriptor page; 236 237 NewPageAction(DataPageDescriptor page) { 238 super(page.getName(), page.getImageDescriptor()); 239 this.page = page; 240 } 241 242 @Override 243 public void run() { 244 addChildToSelected(page); 245 } 246 } 247 248 public static final String Outline_TREE_NAME = "org.openjdk.jmc.flightrecorder.ui.editor.JfrOutlineTree"; //$NON-NLS-1$ 249 250 private static final int DND_OPERATIONS = DND.DROP_MOVE | DND.DROP_COPY; 251 private static final Transfer[] DND_TRANSFER = new Transfer[] {LocalSelectionTransfer.getTransfer()}; 252 private static final ImageDescriptor RESET_ICON = UIPlugin.getDefault() 253 .getMCImageDescriptor(UIPlugin.ICON_RESET_TO_DEFAULTS); 254 private static final IAction RESET_ALL_PAGES_ACTION = new Action(Messages.JFR_OUTLINE_RESET_ALL_ACTION, 255 RESET_ICON) { 256 257 @Override 258 public void run() { 259 if (MessageDialog.openConfirm(Display.getCurrent().getActiveShell(), 260 Messages.JFR_OUTLINE_RESET_ALL_CONFIRM_TITLE, Messages.JFR_OUTLINE_RESET_ALL_CONFIRM_MESSAGE)) { 261 FlightRecorderUI.getDefault().getPageManager().reset(); 262 } 263 }; 264 }; 265 private static final IAction PAGE_STRUCTURE_LOCK_ACTION = createPageStructureLockAction(); 266 267 private final JfrEditor editor; 268 private IPropertyChangeListener analysisEnabledListener; 269 270 public JfrOutlinePage(JfrEditor editor) { 271 this.editor = editor; 272 273 analysisEnabledListener = e -> { 274 if (e.getProperty().equals(PreferenceKeys.PROPERTY_ENABLE_RECORDING_ANALYSIS)) { 275 DisplayToolkit.safeAsyncExec(() -> refresh(true)); 276 } 277 }; 278 FlightRecorderUI.getDefault().getPreferenceStore().addPropertyChangeListener(analysisEnabledListener); 279 } 280 281 @Override 282 public void init(IPageSite pageSite) { 283 super.init(pageSite); 284 pageSite.setSelectionProvider(editor.getSite().getSelectionProvider()); 285 } 286 287 @Override 288 public void createControl(Composite parent) { 289 super.createControl(parent); 290 getTreeViewer().getTree().setData("name", Outline_TREE_NAME); //$NON-NLS-1$ 291 292 getTreeViewer().setContentProvider(CONTENT_PROVIDER); 293 getTreeViewer().setLabelProvider(new OutlineLabelProvider()); 294 PlatformUI.getWorkbench().getHelpSystem().setHelp(getTreeViewer().getControl(), HELP_CONTEXT_ID); 295 296 IAction copyAction = ActionToolkit.commandAction(this::copyToClipboard, EDIT_COPY); 297 IAction pasteAction = ActionToolkit.commandAction(this::pasteFromClipboard, EDIT_PASTE); 298 IAction deleteAction = ActionToolkit.commandAction(this::deleteSelected, EDIT_DELETE); 299 300 IActionBars ab = getSite().getActionBars(); 301 ab.setGlobalActionHandler(copyAction.getActionDefinitionId(), copyAction); 302 ab.setGlobalActionHandler(pasteAction.getActionDefinitionId(), pasteAction); 303 ab.setGlobalActionHandler(deleteAction.getActionDefinitionId(), deleteAction); 304 ab.updateActionBars(); 305 306 MCContextMenuManager mm = MCContextMenuManager.create(getTreeViewer().getControl()); 307 mm.appendToGroup(MCContextMenuManager.GROUP_NEW, createNewPageMenuManager()); 308 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, 309 ActionToolkit.action(() -> moveSelectedLeft(true), Messages.JFR_OUTLINE_MOVE_LEFT)); 310 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, 311 ActionToolkit.action(() -> moveSelectedLeft(false), Messages.JFR_OUTLINE_MOVE_RIGHT)); 312 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, 313 ActionToolkit.action(() -> moveSelectedUp(true), Messages.JFR_OUTLINE_MOVE_UP)); 314 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, 315 ActionToolkit.action(() -> moveSelectedUp(false), Messages.JFR_OUTLINE_MOVE_DOWN)); 316 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, copyAction); 317 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, pasteAction); 318 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, deleteAction); 319 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, 320 ActionToolkit.action(this::resetSelected, Messages.JFR_OUTLINE_RESET_ACTION, RESET_ICON)); 321 322 ab.getMenuManager().add(createNewPageMenuManager()); 323 ab.getToolBarManager().add(PAGE_STRUCTURE_LOCK_ACTION); 324 ab.getToolBarManager().add(RESET_ALL_PAGES_ACTION); 325 326 getTreeViewer().addDoubleClickListener(e -> expandSelected()); 327 getTreeViewer().addPostSelectionChangedListener((event) -> { 328 Object selected = (((IStructuredSelection) event.getSelection()).getFirstElement()); 329 if (selected != null) { 330 editor.navigateTo((DataPageDescriptor) selected); 331 } 332 }); 333 334 ColumnViewerToolTipSupport.enableFor(getTreeViewer()); 335 336 getTreeViewer().addDragSupport(DND_OPERATIONS, DND_TRANSFER, new OutlineDragListener()); 337 getTreeViewer().addDropSupport(DND_OPERATIONS, DND_TRANSFER, new OutlineDropListener(getTreeViewer())); 338 339 /* 340 * Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=438919 341 * 342 * This bug was introduced in Eclipse 3.6 M6, fixed in 3.6 RC4, reintroduced in 4.4.0, fixed 343 * in 4.4.1. 344 * 345 * While ContentViewer keeps its input in a private field, AbstractTreeViewer.inputChanged() 346 * also stores the root tree element derived from (and often the same as) the input using 347 * Widget.setData() on the tree, without using a key. This allows unified translation from a 348 * widget (Tree or TreeItem) to the corresponding tree element by using getData(), 349 * simplifying code. 350 * 351 * However, in Eclipse 4.4.0, PageBookView.createPage() also uses Widget.setData() without 352 * key on the topmost control of the page to store a ContributionInfo (used by the 353 * Alt+Shift+F3 introspection). Since this control typically is the tree, this would 354 * overwrite the root element of the tree. In particular, this would happen if the tree 355 * input is set during Page.createControl(). This could cause a ContributionInfo to be sent 356 * to ITreeContentProvider.getChildren(). 357 * 358 * The current workaround is to do a refresh asynchronously, but this might not be optimal. 359 */ 360 DisplayToolkit.safeAsyncExec(this::refresh); 361 } 362 363 private MenuManager createNewPageMenuManager() { 364 MenuManager newPageMenu = new MenuManager(Messages.JFR_OUTLINE_NEW_PAGE); 365 newPageMenu.setRemoveAllWhenShown(true); 366 newPageMenu.addMenuListener(this::fillNewPageMenu); 367 return newPageMenu; 368 } 369 370 private void fillNewPageMenu(IMenuManager manager) { 371 manager.add(ActionToolkit.action(this::openCreateCustomPageDialog, Messages.JFR_OUTLINE_CUSTOM_PAGE)); 372 FlightRecorderUI.getDefault().getPageManager().getInitialPages().map(NewPageAction::new).forEach(manager::add); 373 } 374 375 private void copyToClipboard() { 376 Object selected = ((IStructuredSelection) getTreeViewer().getSelection()).getFirstElement(); 377 if (selected != null) { 378 ClipboardManager.setClipboardContents(new Object[] {selected}, 379 new Transfer[] {ClipboardManager.getClipboardLocalTransfer()}); 380 } 381 } 382 383 private void pasteFromClipboard() { 384 Object onClipboard = ClipboardManager.getClipboardContents(ClipboardManager.getClipboardLocalTransfer()); 385 if (onClipboard instanceof DataPageDescriptor) { 386 addChildToSelected((DataPageDescriptor) onClipboard); 387 } 388 } 389 390 private void addChildToSelected(DataPageDescriptor child) { 391 Object selected = ((IStructuredSelection) getTreeViewer().getSelection()).getFirstElement(); 392 if (selected == null) { 393 FlightRecorderUI.getDefault().getPageManager().makeRoot(new DataPageDescriptor(child)); 394 } else if (selected instanceof DataPageDescriptor) { 395 FlightRecorderUI.getDefault().getPageManager().makeChild(new DataPageDescriptor(child), 396 (DataPageDescriptor) selected, 0); 397 getTreeViewer().setExpandedState(selected, true); 398 } 399 } 400 401 private void deleteSelected() { 402 Object selected = ((IStructuredSelection) getTreeViewer().getSelection()).getFirstElement(); 403 if (selected instanceof DataPageDescriptor && MessageDialog.openConfirm(Display.getCurrent().getActiveShell(), 404 Messages.JFR_OUTLINE_DELETE_CONFIRM_TITLE, Messages.JFR_OUTLINE_DELETE_CONFIRM_MESSAGE)) { 405 DataPageDescriptor dpd = (DataPageDescriptor) selected; 406 DataPageDescriptor parent = dpd.getParent(); 407 FlightRecorderUI.getDefault().getPageManager().deletePage(dpd); 408 getTreeViewer().refresh(parent); 409 } 410 } 411 412 private void resetSelected() { 413 Object selected = ((IStructuredSelection) getTreeViewer().getSelection()).getFirstElement(); 414 if (selected instanceof DataPageDescriptor && MessageDialog.openConfirm(Display.getCurrent().getActiveShell(), 415 Messages.JFR_OUTLINE_RESET_CONFIRM_TITLE, Messages.JFR_OUTLINE_RESET_CONFIRM_MESSAGE)) { 416 DataPageDescriptor dpd = (DataPageDescriptor) selected; 417 FlightRecorderUI.getDefault().getPageManager().resetPage(dpd); 418 editor.displayPage(dpd); 419 getTreeViewer().update(dpd, null); 420 } 421 } 422 423 private void moveSelectedUp(boolean up) { 424 Object selected = ((IStructuredSelection) getTreeViewer().getSelection()).getFirstElement(); 425 if (selected instanceof DataPageDescriptor) { 426 DataPageDescriptor dpd = (DataPageDescriptor) selected; 427 FlightRecorderUI.getDefault().getPageManager().makeSibling(dpd, dpd, up ? -1 : 2); 428 } 429 } 430 431 private void moveSelectedLeft(boolean left) { 432 Object selected = ((IStructuredSelection) getTreeViewer().getSelection()).getFirstElement(); 433 if (selected instanceof DataPageDescriptor) { 434 DataPageDescriptor dpd = (DataPageDescriptor) selected; 435 if (!left) { 436 FlightRecorderUI.getDefault().getPageManager().makeChild(dpd, dpd, -1); 437 } else if (dpd.getParent() != null) { 438 FlightRecorderUI.getDefault().getPageManager().makeSibling(dpd, dpd.getParent(), 1); 439 } 440 } 441 } 442 443 private void expandSelected() { 444 Object selected = ((IStructuredSelection) getTreeViewer().getSelection()).getFirstElement(); 445 if (selected != null) { 446 getTreeViewer().setExpandedState(selected, !getTreeViewer().getExpandedState(selected)); 447 } 448 } 449 450 @Override 451 protected int getTreeStyle() { 452 return SWT.VIRTUAL | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.BORDER; 453 } 454 455 @Override 456 public void dispose() { 457 FlightRecorderUI.getDefault().getPreferenceStore().removePropertyChangeListener(analysisEnabledListener); 458 super.dispose(); 459 } 460 461 void refresh() { 462 refresh(false); 463 } 464 465 private void refresh(boolean forceRefreshPages) { 466 TreeViewer tv = getTreeViewer(); 467 DataPageDescriptor editorPage = editor.getCurrentPage(); 468 if (editorPage != null && tv != null && !tv.getControl().isDisposed()) { 469 if (tv.getInput() == null || forceRefreshPages) { 470 tv.setInput(FlightRecorderUI.getDefault().getPageManager().getRootPages()); 471 tv.expandToLevel(2); 472 } else { 473 tv.refresh(); 474 } 475 if (((IStructuredSelection) tv.getSelection()).getFirstElement() != editorPage) { 476 tv.setSelection(new StructuredSelection(editorPage)); 477 } 478 } 479 } 480 481 private static IAction createPageStructureLockAction() { 482 ImageDescriptor lockIcon = UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_LOCK_TREE); 483 IAction lockAction = ActionToolkit.checkAction(FlightRecorderUI.getDefault()::setPageStructureLocked, 484 Messages.JFR_OUTLINE_LOCK_PAGES_ACTION, lockIcon); 485 lockAction.setChecked(FlightRecorderUI.getDefault().isPageStructureLocked()); 486 return lockAction; 487 } 488 489 private void openCreateCustomPageDialog() { 490 TypeSelectorWizardPage.openDialog(editor.getModel().getTypeTree(), types -> { 491 PageManager pm = FlightRecorderUI.getDefault().getPageManager(); 492 addChildToSelected(pm.createPage(ItemHandlerPage.Factory.class, new ItemHandlerUiStandIn(types))); 493 }, Messages.JFR_OUTLINE_CREATE_CUSTOM_TITLE, Messages.JFR_OUTLINE_CREATE_CUSTOM_MESSAGE); 494 } 495 }