/* * Copyright (c) 2015 the original author or authors. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Etienne Studer & Donát Csikós (Gradle Inc.) - initial API and implementation and initial documentation */ package org.eclipse.buildship.ui.notification; import com.google.common.base.Preconditions; import com.google.common.collect.FluentIterable; import org.eclipse.buildship.ui.UiPlugin; import org.eclipse.buildship.ui.i18n.UiMessages; import org.eclipse.buildship.ui.util.font.FontUtils; import org.eclipse.core.runtime.IStatus; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.viewers.*; import org.eclipse.jface.window.SameShellProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.*; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.PlatformUI; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.util.*; import java.util.List; /** * Custom {@link Dialog} implementation showing one or more exceptions. */ public final class ExceptionDetailsDialog extends Dialog { private static final String LINE_SEPARATOR = System.getProperty("line.separator"); //$NON-NLS-1$ private static final int COPY_EXCEPTION_BUTTON_ID = 25; private final String title; private final String message; private final String details; private final ArrayList<Throwable> throwables; private final Image image; private Button detailsButton; private Composite stackTraceAreaControl; private TableViewer exceptionsViewer; private Clipboard clipboard; private Text stacktraceAreaText; private Composite singleErrorContainer; private Composite multiErrorContainer; private StackLayout stackLayout; public ExceptionDetailsDialog(Shell shell, String title, String message, String details, int severity, Throwable throwable) { super(new SameShellProvider(Preconditions.checkNotNull(shell))); this.title = Preconditions.checkNotNull(title); this.message = Preconditions.checkNotNull(message); this.details = Preconditions.checkNotNull(details); this.throwables = new ArrayList<Throwable>(Arrays.asList(throwable)); this.image = getIconForSeverity(severity, shell); setShellStyle(SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL); } @Override protected Control createDialogArea(Composite parent) { Composite dialogArea = (Composite) super.createDialogArea(parent); dialogArea.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // dialog image ((GridLayout) dialogArea.getLayout()).numColumns = 2; Label imageLabel = new Label(dialogArea, 0); this.image.setBackground(imageLabel.getBackground()); imageLabel.setImage(this.image); imageLabel.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER | GridData.VERTICAL_ALIGN_BEGINNING)); // composite to include all text widgets Composite textArea = new Composite(dialogArea, SWT.NONE); GridLayout textAreaLayout = new GridLayout(1, false); textAreaLayout.verticalSpacing = FontUtils.getFontHeightInPixels(parent.getFont()); textAreaLayout.marginWidth = textAreaLayout.marginHeight = 0; textArea.setLayout(textAreaLayout); GridData textAreaLayoutData = new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1); textAreaLayoutData.widthHint = convertHorizontalDLUsToPixels(IDialogConstants.MINIMUM_MESSAGE_AREA_WIDTH); textArea.setLayoutData(textAreaLayoutData); Composite stackLayoutContainer = new Composite(textArea, SWT.NONE); this.stackLayout = new StackLayout(); stackLayoutContainer.setLayout(this.stackLayout); stackLayoutContainer.setLayoutData(new GridData(GridData.FILL_BOTH)); // single error container this.singleErrorContainer = new Composite(stackLayoutContainer, SWT.NONE); GridLayout singleErrorContainerLayout = new GridLayout(1, false); singleErrorContainerLayout.marginWidth = singleErrorContainerLayout.marginHeight = 0; this.singleErrorContainer.setLayout(singleErrorContainerLayout); this.stackLayout.topControl = this.singleErrorContainer; // single error label Label singleErrorMessageLabel = new Label(this.singleErrorContainer, SWT.WRAP); GridData messageLabelGridData = new GridData(); messageLabelGridData.verticalAlignment = SWT.TOP; messageLabelGridData.grabExcessHorizontalSpace = true; singleErrorMessageLabel.setLayoutData(messageLabelGridData); singleErrorMessageLabel.setText(this.message); // single error details Label singleErrorDetailsLabel = new Label(this.singleErrorContainer, SWT.WRAP); GridData detailsLabelGridData = new GridData(); detailsLabelGridData.verticalAlignment = SWT.TOP; detailsLabelGridData.grabExcessHorizontalSpace = true; singleErrorDetailsLabel.setLayoutData(detailsLabelGridData); singleErrorDetailsLabel.setText(this.details); // multi error container this.multiErrorContainer = new Composite(stackLayoutContainer, SWT.NONE); GridLayout multiErrorContainerLayout = new GridLayout(1, false); multiErrorContainerLayout.marginWidth = multiErrorContainerLayout.marginHeight = 0; this.multiErrorContainer.setLayout(multiErrorContainerLayout); // multi error label Label multiErrorMessageLabel = new Label(this.multiErrorContainer, SWT.WRAP); multiErrorMessageLabel.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); multiErrorMessageLabel.setText(this.message); // multi error messages displayed in a list viewer GridData multiErrorExceptionListGridData = new GridData(); multiErrorExceptionListGridData.horizontalAlignment = SWT.FILL; multiErrorExceptionListGridData.verticalAlignment = SWT.FILL; multiErrorExceptionListGridData.grabExcessHorizontalSpace = true; multiErrorExceptionListGridData.grabExcessVerticalSpace = true; multiErrorExceptionListGridData.widthHint = 800; this.exceptionsViewer = new TableViewer(this.multiErrorContainer, SWT.MULTI); this.exceptionsViewer.getControl().setLayoutData(multiErrorExceptionListGridData); this.exceptionsViewer.setContentProvider(new ArrayContentProvider()); this.exceptionsViewer.setLabelProvider(new LabelProvider() { @Override public String getText(Object element) { if (element instanceof Throwable) { return ((Throwable) element).getMessage(); } else { return ""; } } }); this.exceptionsViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { updateStacktraceArea(); } }); // set clipboard this.clipboard = new Clipboard(getShell().getDisplay()); // update updateDisplayedExceptions(); return dialogArea; } @Override protected void createButtonsForButtonBar(Composite parent) { Button copyExceptionButton = createButton(parent, COPY_EXCEPTION_BUTTON_ID, "", false); copyExceptionButton.setToolTipText(UiMessages.Button_CopyFailuresToClipboard_Tooltip); copyExceptionButton.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_TOOL_COPY)); this.detailsButton = createButton(parent, IDialogConstants.DETAILS_ID, IDialogConstants.SHOW_DETAILS_LABEL, false); Button okButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); okButton.setFocus(); } @Override protected void setButtonLayoutData(Button button) { if (button.getData() != null && button.getData().equals(COPY_EXCEPTION_BUTTON_ID)) { // do not set a width hint for the copy error button, like it is done in the super // implementation GridDataFactory.swtDefaults().applyTo(button); return; } super.setButtonLayoutData(button); } @Override protected void initializeBounds() { // do not make columns equal width so that we can have a smaller 'copy failure' button Composite buttonBar = (Composite) getButtonBar(); GridLayout layout = (GridLayout) buttonBar.getLayout(); layout.makeColumnsEqualWidth = false; super.initializeBounds(); } @Override protected void buttonPressed(int id) { if (id == IDialogConstants.DETAILS_ID) { toggleStacktraceArea(); } else if (id == COPY_EXCEPTION_BUTTON_ID) { copyStacktracesToClipboard(); } else { super.buttonPressed(id); } } @Override public boolean close() { if (this.clipboard != null) { this.clipboard.dispose(); this.clipboard = null; } return super.close(); } private Image getIconForSeverity(int severity, Shell shell) { int swtImageKey; switch (severity) { case IStatus.OK: case IStatus.INFO: swtImageKey = SWT.ICON_INFORMATION; break; case IStatus.WARNING: case IStatus.CANCEL: swtImageKey = SWT.ICON_WARNING; break; case IStatus.ERROR: swtImageKey = SWT.ICON_ERROR; break; default: // for unknown severity display the warning image swtImageKey = SWT.ICON_WARNING; UiPlugin.logger().warn("Can't find image for severity: " + severity); } return shell.getDisplay().getSystemImage(swtImageKey); } /** * Adds a new exception to the dialog. If the dialog is not yet visible the exception is stored * and will be displayed after the client calls {@link #open()}. * * @param throwable the exception to show */ public void addException(Throwable throwable) { final Throwable exception = throwable; // save the new exceptions this.throwables.add(exception); // update the UI elements if the dialog was already opened if (getContents() != null) { updateDisplayedExceptions(); } } private void updateDisplayedExceptions() { // set the input for the exception list setExceptionsViewerInput(this.throwables); if (this.throwables.size() > 1) { setDialogTitle(UiMessages.Dialog_Title_Multiple_Errors); showMultiError(); } else { setDialogTitle(this.title); showSingleError(); } } private void toggleStacktraceArea() { if (isStacktraceAreaVisible()) { hideStacktraceArea(); } else { showStacktraceArea(); updateStacktraceArea(); } relayoutShell(); } private void relayoutShell() { // compute the new window size Point oldSize = getContents().getSize(); Point newSize = getContents().computeSize(SWT.DEFAULT, SWT.DEFAULT); Point oldWindowSize = getShell().getSize(); Point newWindowSize = new Point(oldWindowSize.x, oldWindowSize.y + (newSize.y - oldSize.y)); // crop new window size to screen Point windowLocation = getShell().getLocation(); Rectangle screenArea = getContents().getDisplay().getClientArea(); if (newWindowSize.y > screenArea.height - (windowLocation.y - screenArea.y)) { newWindowSize.y = screenArea.height - (windowLocation.y - screenArea.y); } getShell().setSize(newWindowSize); ((Composite) getContents()).layout(); } private void updateStacktraceArea() { // show only the selected exceptions in the dialog area or all of them if nothing // is selected Collection<Throwable> selectedExceptions = getSelectedExceptionsFromViewer(); if (selectedExceptions.isEmpty()) { selectedExceptions = this.throwables; } setStacktraceAreaText(collectStackTraces(selectedExceptions)); } private boolean isStacktraceAreaVisible() { return this.stackTraceAreaControl != null; } private void showStacktraceArea() { // create the stacktrace container area this.stackTraceAreaControl = new Composite((Composite) getContents(), SWT.NONE); this.stackTraceAreaControl.setLayoutData(new GridData(GridData.FILL_BOTH)); GridLayout containerLayout = new GridLayout(); containerLayout.marginHeight = containerLayout.marginWidth = 0; this.stackTraceAreaControl.setLayout(containerLayout); // the text inside the stacktrace area this.stacktraceAreaText = new Text(this.stackTraceAreaControl, SWT.MULTI | SWT.READ_ONLY | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL); this.stacktraceAreaText.setLayoutData(new GridData(GridData.FILL_BOTH)); // update button this.detailsButton.setText(IDialogConstants.HIDE_DETAILS_LABEL); } private String collectStackTraces(Collection<Throwable> throwables) { Writer writer = new StringWriter(1024); PrintWriter printWriter = new PrintWriter(writer); for (Throwable throwable : throwables) { throwable.printStackTrace(printWriter); printWriter.write(LINE_SEPARATOR); } return writer.toString(); } private void copyStacktracesToClipboard() { StringBuilder sb = new StringBuilder(); sb.append(this.message); sb.append(LINE_SEPARATOR); sb.append(this.details); sb.append(LINE_SEPARATOR); sb.append(collectStackTraces(this.throwables)); setClipboardContent(sb.toString()); } private void hideStacktraceArea() { this.stackTraceAreaControl.dispose(); this.stackTraceAreaControl = null; this.detailsButton.setText(IDialogConstants.SHOW_DETAILS_LABEL); } private void showSingleError() { if (this.stackLayout != null && isAccessible(this.singleErrorContainer)) { this.stackLayout.topControl = this.singleErrorContainer; this.singleErrorContainer.getParent().layout(); } } private void showMultiError() { if (this.stackLayout != null && isAccessible(this.multiErrorContainer)) { this.stackLayout.topControl = this.multiErrorContainer; this.multiErrorContainer.getParent().layout(); } } private Collection<Throwable> getSelectedExceptionsFromViewer() { if (isAccessible(this.exceptionsViewer)) { ISelection selection = this.exceptionsViewer.getSelection(); if (selection instanceof IStructuredSelection) { @SuppressWarnings("unchecked") List<Object> structuredSelection = ((IStructuredSelection) selection).toList(); return FluentIterable.<Object>from(structuredSelection).filter(Throwable.class).toList(); } } // if nothing is selected then return all available exceptions return Collections.emptyList(); } private void setClipboardContent(String content) { if (this.clipboard != null && !this.clipboard.isDisposed()) { this.clipboard.setContents(new String[] { content }, new Transfer[] { TextTransfer.getInstance() }); } } private void setExceptionsViewerInput(Collection<Throwable> input) { if (isAccessible(this.exceptionsViewer)) { this.exceptionsViewer.setInput(input); } } private void setStacktraceAreaText(String text) { if (isAccessible(this.stacktraceAreaText)) { this.stacktraceAreaText.setText(text); } } private void setDialogTitle(String title) { Shell control = getShell(); if (isAccessible(control)) { control.setText(title); } } private static boolean isAccessible(Widget widget) { return widget != null && !widget.isDisposed(); } private static boolean isAccessible(Viewer widget) { return widget != null && isAccessible(widget.getControl()); } }