| /******************************************************************************* |
| * Copyright (c) 2003, 2015 IBM Corporation and others. |
| * 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: |
| * 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 java.util.Comparator; |
| 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.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.widgets.Control; |
| 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.AnimationEngine; |
| import org.eclipse.ui.internal.WorkbenchPlugin; |
| import org.eclipse.ui.internal.WorkbenchWindow; |
| 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 { |
| |
| @SuppressWarnings("unchecked") |
| static class ProgressViewerComparator extends ViewerComparator { |
| @Override |
| @SuppressWarnings("rawtypes") |
| 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, new Comparator<Object>() { |
| @Override |
| public int compare(Object a, Object b) { |
| return 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 = gc.getFontMetrics().getAverageCharWidth(); |
| 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; |
| } |
| } |
| } |
| } |
| |
| s = clipToLength(textValue, ellipsisString, pivot, currentLength); |
| return s; |
| } |
| |
| 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$ |
| } |
| String s; |
| 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$ |
| } |
| s = s1 + ellipsisString + s2; |
| return s; |
| } |
| |
| /** |
| * 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 |
| * @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()) { |
| return getModalChildExcluding(PlatformUI.getWorkbench() |
| .getDisplay().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)) { |
| 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 e) { |
| // Use non-modal shell |
| } catch (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; |
| } |
| |
| /** |
| * Animate the closing of a window given the start position down to the |
| * progress region. |
| * |
| * @param startPosition |
| * Rectangle. The position to start drawing from. |
| */ |
| public static void animateDown(Rectangle startPosition) { |
| IWorkbenchWindow currentWindow = PlatformUI.getWorkbench() |
| .getActiveWorkbenchWindow(); |
| if (currentWindow == null) { |
| return; |
| } |
| WorkbenchWindow internalWindow = (WorkbenchWindow) currentWindow; |
| |
| ProgressRegion progressRegion = internalWindow.getProgressRegion(); |
| if (progressRegion == null) { |
| return; |
| } |
| Rectangle endPosition = progressRegion.getControl().getBounds(); |
| |
| Point windowLocation = internalWindow.getShell().getLocation(); |
| endPosition.x += windowLocation.x; |
| endPosition.y += windowLocation.y; |
| |
| // animate the progress dialog's removal |
| AnimationEngine.createTweakedAnimation(internalWindow.getShell(), 400, startPosition, endPosition); |
| } |
| |
| /** |
| * Animate the opening of a window given the start position down to the |
| * progress region. |
| * |
| * @param endPosition |
| * Rectangle. The position to end drawing at. |
| */ |
| public static void animateUp(Rectangle endPosition) { |
| IWorkbenchWindow currentWindow = PlatformUI.getWorkbench() |
| .getActiveWorkbenchWindow(); |
| if (currentWindow == null) { |
| return; |
| } |
| WorkbenchWindow internalWindow = (WorkbenchWindow) currentWindow; |
| Point windowLocation = internalWindow.getShell().getLocation(); |
| |
| ProgressRegion progressRegion = internalWindow.getProgressRegion(); |
| if (progressRegion == null) { |
| return; |
| } |
| Rectangle startPosition = progressRegion.getControl().getBounds(); |
| startPosition.x += windowLocation.x; |
| startPosition.y += windowLocation.y; |
| |
| // animate the progress dialog's arrival |
| AnimationEngine.createTweakedAnimation(internalWindow.getShell(), 400, startPosition, endPosition); |
| } |
| |
| /** |
| * 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 new IShellProvider() { |
| |
| @Override |
| public Shell getShell() { |
| return 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; |
| } |
| } |
| } |