| /******************************************************************************* |
| * Copyright (c) 2003, 2019 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Lars Vogel <Lars.Vogel@gmail.com> - Bug 422040 |
| *******************************************************************************/ |
| package org.eclipse.ui.internal.progress; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.Arrays; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.QualifiedName; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.e4.ui.model.application.MApplication; |
| import org.eclipse.e4.ui.model.application.ui.basic.MWindow; |
| import org.eclipse.jface.viewers.Viewer; |
| import org.eclipse.jface.viewers.ViewerComparator; |
| import org.eclipse.jface.window.IShellProvider; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.ui.IWorkbenchPage; |
| import org.eclipse.ui.IWorkbenchWindow; |
| import org.eclipse.ui.PartInitException; |
| import org.eclipse.ui.PlatformUI; |
| import org.eclipse.ui.internal.WorkbenchPlugin; |
| import org.eclipse.ui.internal.misc.StatusUtil; |
| import org.eclipse.ui.internal.util.BundleUtility; |
| import org.eclipse.ui.progress.IProgressConstants; |
| import org.eclipse.ui.views.IViewDescriptor; |
| |
| /** |
| * The ProgressUtil is a class that contains static utility methods used for the |
| * progress API. |
| */ |
| |
| public class ProgressManagerUtil { |
| |
| static class ProgressViewerComparator extends ViewerComparator { |
| @Override |
| @SuppressWarnings({ "rawtypes", "unchecked" }) |
| public int compare(Viewer testViewer, Object e1, Object e2) { |
| return ((Comparable) e1).compareTo(e2); |
| } |
| |
| @Override |
| public void sort(final Viewer viewer, Object[] elements) { |
| /* |
| * https://bugs.eclipse.org/371354 |
| * |
| * This ordering is inherently unstable, since it relies on modifiable |
| * properties of the elements: E.g. the default implementation in JobTreeElement |
| * compares getDisplayString(), many of whose implementations use |
| * getPercentDone(). |
| * |
| * JavaSE 7+'s TimSort introduced a breaking change: It now throws a new |
| * IllegalArgumentException for bad comparators. Workaround is to retry a few |
| * times. |
| */ |
| for (int retries = 3; retries > 0; retries--) { |
| try { |
| Arrays.sort(elements, (a, b) -> ProgressViewerComparator.this.compare(viewer, a, b)); |
| return; // success |
| } catch (IllegalArgumentException e) { |
| // retry |
| } |
| } |
| |
| // One last try that will log and throw TimSort's IAE if it happens: |
| super.sort(viewer, elements); |
| } |
| } |
| |
| /** |
| * A constant used by the progress support to determine if an operation is too |
| * short to show progress. |
| */ |
| public static long SHORT_OPERATION_TIME = 250; |
| |
| static final QualifiedName KEEP_PROPERTY = IProgressConstants.KEEP_PROPERTY; |
| |
| static final QualifiedName KEEPONE_PROPERTY = IProgressConstants.KEEPONE_PROPERTY; |
| |
| static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; |
| |
| private static String ellipsis = ProgressMessages.ProgressFloatingWindow_EllipsisValue; |
| |
| /** |
| * Return a status for the exception. |
| * |
| * @param exception |
| * @return IStatus |
| */ |
| static IStatus exceptionStatus(Throwable exception) { |
| return StatusUtil.newStatus(IStatus.ERROR, exception.getMessage() == null ? "" : exception.getMessage(), //$NON-NLS-1$ |
| exception); |
| } |
| |
| /** |
| * Log the exception for debugging. |
| * |
| * @param exception |
| */ |
| static void logException(Throwable exception) { |
| BundleUtility.log(PlatformUI.PLUGIN_ID, exception); |
| } |
| |
| // /** |
| // * Sets the label provider for the viewer. |
| // * |
| // * @param viewer |
| // */ |
| // static void initLabelProvider(ProgressTreeViewer viewer) { |
| // viewer.setLabelProvider(new ProgressLabelProvider()); |
| // } |
| /** |
| * Return a viewer comparator for looking at the jobs. |
| * |
| * @return ViewerComparator |
| */ |
| static ViewerComparator getProgressViewerComparator() { |
| return new ProgressViewerComparator(); |
| } |
| |
| /** |
| * Open the progress view in the supplied window. |
| * |
| * @param window |
| */ |
| static void openProgressView(IWorkbenchWindow window) { |
| IWorkbenchPage page = window.getActivePage(); |
| if (page == null) { |
| return; |
| } |
| try { |
| IViewDescriptor reference = WorkbenchPlugin.getDefault().getViewRegistry() |
| .find(IProgressConstants.PROGRESS_VIEW_ID); |
| |
| if (reference == null) { |
| return; |
| } |
| page.showView(IProgressConstants.PROGRESS_VIEW_ID); |
| } catch (PartInitException exception) { |
| logException(exception); |
| } |
| } |
| |
| /** |
| * Shorten the given text <code>t</code> so that its length doesn't exceed the |
| * given width. The default implementation replaces characters in the center of |
| * the original string with an ellipsis ("..."). Override if you need a |
| * different strategy. |
| * |
| * @param textValue |
| * @param control |
| * @return String |
| */ |
| static String shortenText(String textValue, Control control) { |
| if (textValue == null) { |
| return null; |
| } |
| int maxWidth = control.getBounds().width - 5; |
| String ellipsisString = ellipsis; |
| GC gc = new GC(control); |
| try { |
| return clipToSize(gc, textValue, ellipsisString, maxWidth); |
| } finally { |
| gc.dispose(); |
| } |
| } |
| |
| private static String clipToSize(GC gc, String textValue, String ellipsisString, int maxWidth) { |
| int averageCharWidth = (int) gc.getFontMetrics().getAverageCharacterWidth(); |
| int length = textValue.length(); |
| |
| int secondWord = findSecondWhitespace(textValue, gc, maxWidth); |
| int pivot = ((length - secondWord) / 2) + secondWord; |
| |
| int currentLength; |
| int upperBoundWidth; |
| int upperBoundLength = 0; |
| |
| // Now use newton's method to search for the correct string size |
| int lowerBoundLength = 0; |
| int lowerBoundWidth = 0; |
| |
| // Try to guess the size of the string based on the font's average |
| // character width |
| int estimatedCharactersThatWillFit = maxWidth / averageCharWidth; |
| |
| if (estimatedCharactersThatWillFit >= length) { |
| int maxExtent = gc.textExtent(textValue).x; |
| if (maxExtent <= maxWidth) { |
| return textValue; |
| } |
| currentLength = Math.max(0, Math.round(length * ((float) maxWidth / maxExtent)) - ellipsisString.length()); |
| upperBoundWidth = maxExtent; |
| upperBoundLength = length; |
| } else { |
| currentLength = Math.min(length, Math.max(0, estimatedCharactersThatWillFit - ellipsisString.length())); |
| for (;;) { |
| String s = clipToLength(textValue, ellipsisString, pivot, currentLength); |
| int currentExtent = gc.textExtent(s).x; |
| if (currentExtent > maxWidth) { |
| upperBoundWidth = currentExtent; |
| upperBoundLength = currentLength; |
| break; |
| } |
| if (currentLength == length) { |
| // No need to clip the string if the whole thing fits. |
| return textValue; |
| } |
| lowerBoundWidth = currentExtent; |
| lowerBoundLength = currentLength; |
| currentLength = Math.min(length, currentLength * 2 + 1); |
| } |
| } |
| |
| String s; |
| for (;;) { |
| int oldLength = currentLength; |
| s = clipToLength(textValue, ellipsisString, pivot, currentLength); |
| |
| int l = gc.textExtent(s).x; |
| int tooBigBy = l - maxWidth; |
| if (tooBigBy == 0) { |
| // If this was exactly the right size, stop the binary |
| // search |
| break; |
| } else if (tooBigBy > 0) { |
| // The string is too big. Need to clip more. |
| upperBoundLength = currentLength; |
| upperBoundWidth = l; |
| if (currentLength <= lowerBoundLength + 1) { |
| // We're one character away from a value that is known |
| // to clip too much, so opt for clipping slightly too |
| // much |
| currentLength = lowerBoundLength; |
| break; |
| } |
| if (tooBigBy <= averageCharWidth * 2) { |
| currentLength--; |
| } else { |
| int spaceToRightOfLowerBound = maxWidth - lowerBoundWidth; |
| currentLength = lowerBoundLength |
| + (currentLength - lowerBoundLength) * spaceToRightOfLowerBound / (l - lowerBoundWidth); |
| if (currentLength >= oldLength) { |
| currentLength = oldLength - 1; |
| } else if (currentLength <= lowerBoundLength) { |
| currentLength = lowerBoundLength + 1; |
| } |
| } |
| } else { |
| // The string is too small. Need to clip less. |
| lowerBoundLength = currentLength; |
| lowerBoundWidth = l; |
| if (currentLength >= upperBoundLength - 1) { |
| // We're one character away from a value that is known |
| // to clip too little, so opt for clipping slightly |
| // too much |
| currentLength = upperBoundLength - 1; |
| break; |
| } |
| if (-tooBigBy <= averageCharWidth * 2) { |
| currentLength++; |
| } else { |
| currentLength = currentLength |
| + (upperBoundLength - currentLength) * (-tooBigBy) / (upperBoundWidth - l); |
| if (currentLength <= oldLength) { |
| currentLength = oldLength + 1; |
| } else if (currentLength >= upperBoundLength) { |
| currentLength = upperBoundLength - 1; |
| } |
| } |
| } |
| } |
| |
| return clipToLength(textValue, ellipsisString, pivot, currentLength); |
| } |
| |
| private static String clipToLength(String textValue, String ellipsisString, int pivot, int newLength) { |
| return getClippedString(textValue, ellipsisString, pivot, textValue.length() - newLength); |
| } |
| |
| private static String getClippedString(String textValue, String ellipsisString, int pivot, int charsToClip) { |
| int length = textValue.length(); |
| if (charsToClip <= 0) { |
| return textValue; |
| } |
| if (charsToClip >= length) { |
| return ""; //$NON-NLS-1$ |
| } |
| int start = pivot - charsToClip / 2; |
| int end = pivot + (charsToClip + 1) / 2; |
| |
| if (start < 0) { |
| end -= start; |
| start = 0; |
| } |
| if (end < 0) { |
| start -= end; |
| end = 0; |
| } |
| |
| String s1 = textValue.substring(0, start); |
| String s2; |
| if (end < length) { |
| s2 = textValue.substring(end, length); |
| } else { |
| s2 = ""; //$NON-NLS-1$ |
| } |
| return s1 + ellipsisString + s2; |
| } |
| |
| /** |
| * Find the second index of a whitespace. Return the first index if there isn't |
| * one or 0 if there is no space at all. |
| * |
| * @param textValue |
| * @param gc The GC to test max length |
| * @param maxWidth The maximim extent |
| * @return int |
| */ |
| private static int findSecondWhitespace(String textValue, GC gc, int maxWidth) { |
| int firstCharacter = 0; |
| char[] chars = textValue.toCharArray(); |
| // Find the first whitespace |
| for (int i = 0; i < chars.length; i++) { |
| if (Character.isWhitespace(chars[i])) { |
| firstCharacter = i; |
| break; |
| } |
| } |
| // If we didn't find it once don't continue |
| if (firstCharacter == 0) { |
| return 0; |
| } |
| // Initialize to firstCharacter in case there is no more whitespace |
| int secondCharacter = firstCharacter; |
| // Find the second whitespace |
| for (int i = firstCharacter; i < chars.length; i++) { |
| if (Character.isWhitespace(chars[i])) { |
| secondCharacter = i; |
| break; |
| } |
| } |
| // Check that we haven't gone over max width. Throw |
| // out an index that is too high |
| if (gc.textExtent(textValue.substring(0, secondCharacter)).x > maxWidth) { |
| if (gc.textExtent(textValue.substring(0, firstCharacter)).x > maxWidth) { |
| return 0; |
| } |
| return firstCharacter; |
| } |
| return secondCharacter; |
| } |
| |
| /** |
| * If there are any modal shells open reschedule openJob to wait until they are |
| * closed. Return true if it rescheduled, false if there is nothing blocking it. |
| * |
| * @param openJob the job to reschedule (with delay) when modal dialog is open |
| * @return boolean. true if the job was rescheduled due to modal dialogs. |
| */ |
| public static boolean rescheduleIfModalShellOpen(Job openJob) { |
| Shell modal = getModalShellExcluding(null); |
| if (modal == null) { |
| return false; |
| } |
| |
| // try again in a few seconds |
| openJob.schedule(PlatformUI.getWorkbench().getProgressService().getLongOperationTime()); |
| return true; |
| } |
| |
| /** |
| * Return whether or not it is safe to open this dialog. If so then return |
| * <code>true</code>. If not then set it to open itself when it has had |
| * ProgressManager#longOperationTime worth of ticks. |
| * |
| * @param dialog ProgressMonitorJobsDialog that will be opening |
| * @param excludedShell The shell |
| * @return boolean. <code>true</code> if it can open. Otherwise return false and |
| * set the dialog to tick. |
| */ |
| public static boolean safeToOpen(ProgressMonitorJobsDialog dialog, Shell excludedShell) { |
| Shell modal = getModalShellExcluding(excludedShell); |
| if (modal == null) { |
| return true; |
| } |
| |
| dialog.watchTicks(); |
| return false; |
| } |
| |
| /** |
| * Return the modal shell that is currently open. If there isn't one then return |
| * null. If there are stacked modal shells, return the top one. |
| * |
| * @param shell A shell to exclude from the search. May be <code>null</code>. |
| * |
| * @return Shell or <code>null</code>. |
| */ |
| |
| public static Shell getModalShellExcluding(Shell shell) { |
| |
| // If shell is null or disposed, then look through all shells |
| if (shell == null || shell.isDisposed()) { |
| Display display = PlatformUI.getWorkbench().getDisplay(); |
| if (display.isDisposed()) { |
| return null; |
| } |
| return getModalChildExcluding(display.getShells(), shell); |
| } |
| |
| // Start with the shell to exclude and check it's shells |
| return getModalChildExcluding(shell.getShells(), shell); |
| } |
| |
| /** |
| * Return the modal shell that is currently open. If there isn't one then return |
| * null. |
| * |
| * @param toSearch shells to search for modal children |
| * @param toExclude shell to ignore |
| * @return the most specific modal child, or null if none |
| */ |
| private static Shell getModalChildExcluding(Shell[] toSearch, Shell toExclude) { |
| int modal = SWT.APPLICATION_MODAL | SWT.SYSTEM_MODAL | SWT.PRIMARY_MODAL; |
| |
| // Make sure we don't pick a parent that has a modal child (this can |
| // lock the app) |
| // If we picked a parent with a modal child, use the modal child instead |
| |
| for (int i = toSearch.length - 1; i >= 0; i--) { |
| Shell shell = toSearch[i]; |
| if (shell.equals(toExclude) || shell.isDisposed()) { |
| continue; |
| } |
| |
| // Check if this shell has a modal child |
| Shell[] children = shell.getShells(); |
| Shell modalChild = getModalChildExcluding(children, toExclude); |
| if (modalChild != null) { |
| return modalChild; |
| } |
| |
| // If not, check if this shell is modal itself |
| if (shell.isVisible() && (shell.getStyle() & modal) != 0) { |
| return shell; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Utility method to get the best parenting possible for a dialog. If there is a |
| * modal shell return it so as to avoid two modal dialogs. If not then return |
| * the shell of the active workbench window. If that shell is <code>null</code> |
| * or not visible, then return the splash shell if still visible. Otherwise |
| * return the shell of the active workbench window. |
| * |
| * @return the best parent shell or <code>null</code> |
| */ |
| public static Shell getDefaultParent() { |
| Shell modal = getModalShellExcluding(null); |
| if (modal != null) { |
| return modal; |
| } |
| |
| Shell nonModalShell = getNonModalShell(); |
| if (nonModalShell != null && nonModalShell.isVisible()) |
| return nonModalShell; |
| |
| try { |
| Shell splashShell = WorkbenchPlugin.getSplashShell(PlatformUI.getWorkbench().getDisplay()); |
| if (splashShell != null && splashShell.isVisible()) { |
| return splashShell; |
| } |
| } catch (IllegalAccessException | InvocationTargetException e) { |
| // Use non-modal shell |
| } |
| |
| return nonModalShell; |
| } |
| |
| /** |
| * Get the active non modal shell. If there isn't one return null. |
| * |
| * @return Shell |
| */ |
| public static Shell getNonModalShell() { |
| MApplication application = PlatformUI.getWorkbench().getService(MApplication.class); |
| if (application == null) { |
| // better safe than sorry |
| return null; |
| } |
| MWindow window = application.getSelectedElement(); |
| if (window != null) { |
| Object widget = window.getWidget(); |
| if (widget instanceof Shell) { |
| return (Shell) widget; |
| } |
| } |
| for (MWindow child : application.getChildren()) { |
| Object widget = child.getWidget(); |
| if (widget instanceof Shell) { |
| return (Shell) widget; |
| } |
| } |
| return null; |
| } |
| |
| |
| /** |
| * Get the shell provider to use in the progress support dialogs. This provider |
| * will try to always parent off of an existing modal shell. If there isn't one |
| * it will use the current workbench window. |
| * |
| * @return IShellProvider |
| */ |
| static IShellProvider getShellProvider() { |
| return ProgressManagerUtil::getDefaultParent; |
| } |
| |
| /** |
| * Get the icons root for the progress support. |
| * |
| * @return URL |
| */ |
| public static URL getIconsRoot() { |
| return BundleUtility.find(PlatformUI.PLUGIN_ID, ProgressManager.PROGRESS_FOLDER); |
| } |
| |
| /** |
| * Return the location of the progress spinner. |
| * |
| * @return URL or <code>null</code> if it cannot be found |
| */ |
| public static URL getProgressSpinnerLocation() { |
| try { |
| return new URL(getIconsRoot(), "progress_spinner.png");//$NON-NLS-1$ |
| } catch (MalformedURLException e) { |
| return null; |
| } |
| } |
| } |