blob: ce633eb5d4152e4432013049da690b94221008a5 [file] [log] [blame]
/*
* Copyright (c) 2014-2018 Eike Stepper (Loehne, Germany) and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Eike Stepper - initial API and implementation
*/
package org.eclipse.oomph.ui;
import org.eclipse.oomph.internal.ui.UIPlugin;
import org.eclipse.oomph.util.ReflectUtil;
import org.eclipse.oomph.util.StringUtil;
import org.eclipse.emf.edit.provider.IItemFontProvider;
import org.eclipse.emf.edit.ui.provider.DiagnosticDecorator;
import org.eclipse.emf.edit.ui.provider.ExtendedFontRegistry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.FontRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Resource;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.services.IServiceLocator;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML.Tag;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
/**
* @author Eike Stepper
*/
public final class UIUtil
{
public static final IWorkbench WORKBENCH;
private static final int COLOR_TRANSPARENT;
private static Image errorImage;
private static Image warningImage;
private static Image infoImage;
private static Boolean browserAvailable;
static
{
IWorkbench workbench = null;
int colorTransparent = -1;
try
{
workbench = PlatformUI.getWorkbench();
}
catch (Throwable ex)
{
// Workbench has not been created.
}
try
{
colorTransparent = ReflectUtil.getValue("COLOR_TRANSPARENT", SWT.class);
}
catch (Throwable ex)
{
// This version of SWT doesn't support a transparent color.
}
WORKBENCH = workbench;
COLOR_TRANSPARENT = colorTransparent;
}
private UIUtil()
{
}
public static Display getDisplay()
{
Display display = Display.getCurrent();
if (display == null)
{
try
{
display = PlatformUI.getWorkbench().getDisplay();
}
catch (Throwable ignore)
{
//$FALL-THROUGH$
}
}
if (display == null)
{
display = Display.getDefault();
}
if (display == null)
{
display = new Display();
}
return display;
}
public static Shell getShell()
{
final Shell[] shell = { null };
final Display display = getDisplay();
display.syncExec(new Runnable()
{
public void run()
{
shell[0] = display.getActiveShell();
if (shell[0] == null)
{
try
{
IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
if (window == null)
{
IWorkbenchWindow[] windows = PlatformUI.getWorkbench().getWorkbenchWindows();
if (windows.length != 0)
{
window = windows[0];
}
}
if (window != null)
{
shell[0] = window.getShell();
}
}
catch (Throwable ignore)
{
//$FALL-THROUGH$
}
}
if (shell[0] == null)
{
Shell[] shells = display.getShells();
if (shells.length > 0)
{
shell[0] = shells[0];
}
}
}
});
return shell[0];
}
public static synchronized boolean isBrowserAvailable()
{
if (browserAvailable == null)
{
syncExec(new Runnable()
{
public void run()
{
Shell shell = null;
try
{
shell = new Shell();
new Browser(shell, SWT.NONE);
browserAvailable = true;
}
catch (SWTError ex)
{
browserAvailable = false;
}
finally
{
try
{
shell.dispose();
}
catch (Exception ex)
{
// Ignore.
}
}
}
});
}
return browserAvailable;
}
/**
* Checks if the given {@link Control} is a child of the given
* parent.
*
* @param parent The parent, not <code>null</code>.
* @param controlToCheck The control to check, not <code>null</code>.
*
* @return <code>true</code> if the given control is a child of the given
* parent, <code>false</code> otherwise.
*/
public static boolean isParent(Composite parent, Control controlToCheck)
{
if (parent == null || controlToCheck == null)
{
throw new IllegalArgumentException("Neither parent nor controlToCheck must be null");
}
if (controlToCheck == parent)
{
return true;
}
Composite tmpParent = controlToCheck.getParent();
while (tmpParent != parent && tmpParent != null)
{
tmpParent = tmpParent.getParent();
}
return tmpParent == parent;
}
public static GridLayout createGridLayout(int numColumns)
{
GridLayout layout = new GridLayout(numColumns, false);
layout.marginWidth = 0;
layout.marginHeight = 0;
return layout;
}
public static GridData applyGridData(Control control)
{
GridData data = new GridData();
data.grabExcessHorizontalSpace = true;
data.horizontalAlignment = GridData.FILL;
control.setLayoutData(data);
return data;
}
public static GridData grabVertical(GridData data)
{
data.grabExcessVerticalSpace = true;
data.verticalAlignment = GridData.FILL;
return data;
}
public static void clearTextSelection(Object control)
{
Text text = findTextControl(control);
if (text != null)
{
text.clearSelection();
}
}
public static void setSelectionToEnd(Widget control)
{
Text text = findTextControl(control);
if (text != null)
{
String content = text.getText();
text.setSelection(content.length() + 1);
}
}
public static void setSelectionTo(Widget control, Point selection)
{
Text text = findTextControl(control);
if (text != null)
{
text.setSelection(selection);
}
}
public static void selectAllText(Widget control)
{
Text text = findTextControl(control);
if (text != null)
{
text.selectAll();
}
}
private static Text findTextControl(Object control)
{
if (control instanceof Viewer)
{
control = ((Viewer)control).getControl();
}
if (control instanceof CCombo)
{
CCombo combo = (CCombo)control;
try
{
control = ReflectUtil.getValue("text", combo);
}
catch (Throwable ex)
{
//$FALL-THROUGH$
}
}
if (control instanceof Text)
{
return (Text)control;
}
return null;
}
public static void runInProgressDialog(Shell shell, IRunnableWithProgress runnable) throws InvocationTargetException, InterruptedException
{
ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell)
{
@Override
protected Point getInitialSize()
{
Point calculatedSize = super.getInitialSize();
if (calculatedSize.x < 800)
{
calculatedSize.x = 800;
}
return calculatedSize;
}
};
try
{
dialog.run(true, true, runnable);
}
catch (OperationCanceledException ex)
{
// Ignore.
}
catch (InvocationTargetException ex)
{
if (!(ex.getCause() instanceof OperationCanceledException))
{
throw ex;
}
}
}
public static void handleException(Throwable ex)
{
UIPlugin.INSTANCE.log(ex);
ErrorDialog.open(ex);
}
public static Color getEclipseThemeColor()
{
return UIPlugin.getColor(44, 34, 85);
}
/**
* Extracts all sprites from the given textureatlas.
*
* @param textureAtlas The input texture atlas.
* @param countX The number of sprites in horizontal axis.
* @param countY The number of sprites in vertical axis.
*
* @return An array containing the countX * countY sprites.
*/
public static Image[] extractSprites(Image textureAtlas, int countX, int countY)
{
Rectangle atlasBounds = textureAtlas.getBounds();
int width = atlasBounds.width / countX;
int height = atlasBounds.height / countY;
Image[] sprites = new Image[countX * countY];
for (int y = 0; y < countY; y++)
{
for (int x = 0; x < countX; x++)
{
sprites[countX * y + x] = UIUtil.extractSprite(textureAtlas, x * width, y * width, width, height);
}
}
return sprites;
}
/**
* Extracts a single sprite from the given texture atlas.
* <br>
* <br>
* <b>Important:</b> the caller is responsible for disposing the created image.
*
* @param textureAtlas The input texture atlas.
* @param x The x coordinate of the target sprite.
* @param y The y coordinate of the target sprite.
* @param width The width of the target sprite.
* @param height The height of the target sprite.
*
* @return A new image with the extracted sprite.
*/
public static Image extractSprite(Image textureAtlas, int x, int y, int width, int height)
{
ImageData textureAtlasData = textureAtlas.getImageData();
PaletteData textureAtlasPaletteData = textureAtlasData.palette;
ImageData spriteImageData = new ImageData(width, height, textureAtlasData.depth,
new PaletteData(textureAtlasPaletteData.redMask, textureAtlasPaletteData.greenMask, textureAtlasPaletteData.blueMask));
int[] pixels = new int[width * height];
byte[] alphas = new byte[width * height];
for (int scanline = 0; scanline < height; scanline++)
{
int yOffset = y + scanline;
textureAtlasData.getPixels(x, yOffset, width, pixels, scanline * width);
textureAtlasData.getAlphas(x, yOffset, width, alphas, scanline * width);
}
spriteImageData.setPixels(0, 0, pixels.length, pixels, 0);
spriteImageData.setAlphas(0, 0, alphas.length, alphas, 0);
return new Image(getDisplay(), spriteImageData);
}
public static Image getStatusImage(int severity)
{
if (severity == IStatus.ERROR)
{
if (errorImage == null)
{
errorImage = UIPlugin.INSTANCE.getSWTImage("error");
}
return errorImage;
}
if (severity == IStatus.WARNING)
{
if (warningImage == null)
{
warningImage = UIPlugin.INSTANCE.getSWTImage("warning");
}
return warningImage;
}
if (infoImage == null)
{
infoImage = UIPlugin.INSTANCE.getSWTImage("info");
}
return infoImage;
}
public static void exec(Display display, boolean async, Runnable runnable)
{
if (async)
{
asyncExec(display, runnable);
}
else
{
syncExec(display, runnable);
}
}
public static void asyncExec(Runnable runnable)
{
final Display display = getDisplay();
if (display != null)
{
asyncExec(display, runnable);
}
}
public static void asyncExec(final Control control, final Runnable runnable)
{
try
{
if (control.isDisposed())
{
return;
}
control.getDisplay().asyncExec(new Runnable()
{
public void run()
{
if (!control.isDisposed())
{
runnable.run();
}
}
});
}
catch (SWTException ex)
{
if (ex.code != SWT.ERROR_WIDGET_DISPOSED)
{
throw ex;
}
//$FALL-THROUGH$
}
}
public static void asyncExec(final Display display, final Runnable runnable)
{
try
{
if (display.isDisposed())
{
return;
}
display.asyncExec(new Runnable()
{
public void run()
{
if (display.isDisposed())
{
return;
}
try
{
runnable.run();
}
catch (SWTException ex)
{
if (ex.code != SWT.ERROR_WIDGET_DISPOSED)
{
throw ex;
}
//$FALL-THROUGH$
}
}
});
}
catch (SWTException ex)
{
if (ex.code != SWT.ERROR_WIDGET_DISPOSED)
{
throw ex;
}
//$FALL-THROUGH$
}
}
public static void syncExec(final Runnable runnable)
{
final Display display = getDisplay();
if (Display.getCurrent() == display || display == null)
{
runnable.run();
}
else
{
syncExec(display, runnable);
}
}
public static void syncExec(final Control control, final Runnable runnable)
{
try
{
if (control.isDisposed())
{
return;
}
control.getDisplay().syncExec(new Runnable()
{
public void run()
{
if (!control.isDisposed())
{
runnable.run();
}
}
});
}
catch (SWTException ex)
{
if (ex.code != SWT.ERROR_WIDGET_DISPOSED)
{
throw ex;
}
//$FALL-THROUGH$
}
}
public static void syncExec(final Display display, final Runnable runnable)
{
try
{
if (display.isDisposed())
{
return;
}
display.syncExec(new Runnable()
{
public void run()
{
if (display.isDisposed())
{
return;
}
try
{
runnable.run();
}
catch (SWTException ex)
{
if (ex.code != SWT.ERROR_WIDGET_DISPOSED)
{
throw ex;
}
//$FALL-THROUGH$
}
}
});
}
catch (SWTException ex)
{
if (ex.code != SWT.ERROR_WIDGET_DISPOSED)
{
throw ex;
}
//$FALL-THROUGH$
}
}
public static void timerExec(int milliseconds, final Runnable runnable)
{
final Display display = getDisplay();
if (display != null)
{
timerExec(milliseconds, display, runnable);
}
}
public static void timerExec(int milliseconds, final Display display, final Runnable runnable)
{
try
{
if (display.isDisposed())
{
return;
}
display.timerExec(milliseconds, new Runnable()
{
public void run()
{
if (display.isDisposed())
{
return;
}
try
{
runnable.run();
}
catch (SWTException ex)
{
if (ex.code != SWT.ERROR_WIDGET_DISPOSED)
{
throw ex;
}
//$FALL-THROUGH$
}
}
});
}
catch (SWTException ex)
{
if (ex.code != SWT.ERROR_WIDGET_DISPOSED)
{
throw ex;
}
//$FALL-THROUGH$
}
}
public static IDialogSettings getOrCreateSection(IDialogSettings settings, String sectionName)
{
IDialogSettings section = settings.getSection(sectionName);
if (section == null)
{
section = settings.addNewSection(sectionName);
}
return section;
}
public static void dispose(Resource... resources)
{
for (int i = 0; i < resources.length; i++)
{
Resource resource = resources[i];
if (resource != null && !resource.isDisposed())
{
resource.dispose();
}
}
}
public static void simulateKey(char character)
{
Display display = getDisplay();
Event event = new Event();
event.type = SWT.KeyDown;
event.character = character;
display.post(event);
try
{
Thread.sleep(10);
}
catch (InterruptedException ex)
{
}
event.type = SWT.KeyUp;
display.post(event);
try
{
Thread.sleep(10);
}
catch (InterruptedException ex)
{
}
}
public static String stripHTML(String html)
{
try
{
final StringBuilder builder = new StringBuilder();
new ParserDelegator().parse(new StringReader(html), new HTMLEditorKit.ParserCallback()
{
@Override
public void handleText(char[] text, int pos)
{
builder.append(text);
}
@Override
public void handleSimpleTag(Tag t, MutableAttributeSet a, int pos)
{
if (t.breaksFlow())
{
builder.append("\n");
}
}
}, Boolean.TRUE);
return builder.toString();
}
catch (IOException ex)
{
return html;
}
}
/**
* This returns HTML that can be rendered by styled text, which respects some HTML but not all of it well.
*/
public static String getRenderableHTML(String html)
{
return stripHTMLFull(html, true);
}
private static String stripHTMLFull(String html, final boolean renderable)
{
try
{
final StringBuilder result = new StringBuilder();
new ParserDelegator().parse(new StringReader(html), new HTMLEditorKit.ParserCallback()
{
private StringBuilder builder = result;
private List<List<StringBuilder>> table;
private List<StringBuilder> row;
@Override
public void handleText(char[] text, int pos)
{
if (renderable)
{
builder.append(DiagnosticDecorator.escapeContent(new String(text)));
}
else
{
builder.append(text);
}
}
@Override
public void handleSimpleTag(Tag t, MutableAttributeSet a, int pos)
{
if ("img".equals(t.toString()))
{
builder.append(" ");
}
else if (t.breaksFlow())
{
if (renderable && builder == result)
{
builder.append("<br/>");
}
builder.append("\n");
}
}
@Override
public void handleStartTag(Tag t, MutableAttributeSet a, int pos)
{
String tagName = t.toString();
if ("table".equals(tagName))
{
table = new ArrayList<List<StringBuilder>>();
}
else if ("tr".equals(tagName))
{
row = new ArrayList<StringBuilder>();
table.add(row);
}
else if ("td".equals(tagName))
{
builder = new StringBuilder();
row.add(builder);
}
else if (tagName.startsWith("h") && tagName.length() > 1 && Character.isDigit(tagName.charAt(1)))
{
appendLineFeed(true);
appendLineFeed(false);
if (renderable)
{
builder.append("<b>");
}
}
else
{
if (t.breaksFlow())
{
appendLineFeed(true);
}
}
}
@Override
public void handleEndTag(Tag t, int pos)
{
String tagName = t.toString();
if ("table".equals(tagName))
{
int[] widths = null;
String[][][] tableEntries = new String[table.size()][][];
builder = result;
int rowIndex = 0;
for (List<StringBuilder> row : table)
{
if (widths == null)
{
widths = new int[row.size()];
}
String[][] tableRow = new String[row.size()][];
tableEntries[rowIndex] = tableRow;
int columnIndex = 0;
for (StringBuilder entry : row)
{
String value = StringUtil.trimRight(entry.toString());
String[] lines = value.split("\n");
tableRow[columnIndex] = lines;
for (String line : lines)
{
widths[columnIndex] = Math.max(widths[columnIndex], line.length());
}
++columnIndex;
}
++rowIndex;
}
if (renderable)
{
builder.append("<pre>");
}
for (String[][] row : tableEntries)
{
int maxLines = 0;
for (String[] column : row)
{
maxLines = Math.max(maxLines, column.length);
}
if (renderable)
{
for (int i = 0; i < maxLines; ++i)
{
int columnIndex = 0;
for (String[] column : row)
{
String columnText = i < column.length ? column[i] : "";
int columnWidth = widths[columnIndex];
append(columnText, columnWidth);
++columnIndex;
}
builder.append("\n");
}
}
else
{
appendLineFeed(true);
for (int i = 0; i < maxLines; ++i)
{
builder.append("| ");
int columnIndex = 0;
for (String[] column : row)
{
append(i < column.length ? column[i] : "", widths[columnIndex]);
builder.append(columnIndex == row.length - 1 ? " |" : " | ");
++columnIndex;
}
appendLineFeed(false);
}
}
}
if (renderable)
{
builder.append("</pre>");
}
table = null;
}
else if ("tr".equals(tagName))
{
}
else if ("td".equals(tagName))
{
}
else if (tagName.startsWith("h") && tagName.length() > 1 && Character.isDigit(tagName.charAt(1)))
{
if (renderable)
{
builder.append("</b>");
appendLineFeed(true);
}
}
else
{
if (t.breaksFlow())
{
appendLineFeed(true);
}
}
}
private void append(String value, int width)
{
builder.append(value);
for (int i = value.length(); i < width; ++i)
{
builder.append(' ');
}
}
private void appendLineFeed(boolean conditional)
{
if (conditional)
{
int length = builder.length();
if (length == 0 || builder.charAt(length - 1) == '\n')
{
return;
}
}
if (renderable)
{
builder.append("<br/>");
}
builder.append('\n');
}
}, Boolean.TRUE);
return result.toString();
}
catch (IOException ex)
{
return html;
}
}
public static Point caclcuateSize(String html)
{
Shell shell = new Shell();
GC gc = new GC(shell);
FontRegistry fontRegistry = JFaceResources.getFontRegistry();
Font font = fontRegistry.get(fontRegistry.hasValueFor("org.eclipse.jdt.ui.javadocfont") ? "org.eclipse.jdt.ui.javadocfont" : JFaceResources.DIALOG_FONT);
Font fixedPitchFont = JFaceResources.getTextFont();
gc.setFont(font);
FontMetrics fontMetrics = gc.getFontMetrics();
@SuppressWarnings("deprecation")
int averageCharWidth = fontMetrics.getAverageCharWidth();
Font boldFont = ExtendedFontRegistry.INSTANCE.getFont(font, IItemFontProvider.BOLD_FONT);
gc.setFont(boldFont);
boolean browerAvailable = isBrowserAvailable();
String text = stripHTMLFull(html, false).trim();
String[] lines = text.split("\n");
int pixelWidth = 0;
for (String line : lines)
{
boolean isTableRow = line.startsWith("|");
if (!browerAvailable && isTableRow)
{
gc.setFont(fixedPitchFont);
}
int lineWidth = gc.textExtent(line).x;
if (browerAvailable && isTableRow)
{
lineWidth += 4 * averageCharWidth;
}
pixelWidth = Math.max(pixelWidth, lineWidth);
gc.setFont(font);
}
int numberOfAverageCharacters = pixelWidth / averageCharWidth + 2;
gc.dispose();
shell.dispose();
return new Point(numberOfAverageCharacters, lines.length);
}
public static void setTransparentBackgroundColor(Control control)
{
if (COLOR_TRANSPARENT != -1)
{
Color transparentColor = control.getDisplay().getSystemColor(COLOR_TRANSPARENT);
control.setBackground(transparentColor);
}
}
public static <T> T getService(IServiceLocator serviceLocator, Class<T> api)
{
return api.cast(serviceLocator.getService(api));
}
/**
* This is a runnable for which {@link #schedule() schedule} can be called repeatedly.
* That will {@link UIUtil#timerExec(int, Runnable) timer execute} this runnable with the delay specified in the constructor,
* or, if the runnable has already been dispatched, will mark it for redispatching.
* As such, when {@link #run() run} is called, if schedule has been called in the meantime,
* the runnable will timer executed again,
* without calling {@link #perform() perform}.
* Otherwise, run will call perform to finally perform the delayed behavior.
* The {@link #prepareForDispatch() prepareForDispatch} method will be called immediate before any timer execute.
* In this default implementation is does nothing.
* If the control specified in the constructor is disposed when run is called, run will do nothing.
*
* @author Ed Merks
*/
public static abstract class DelayedRunnable implements Runnable
{
final private Control control;
private int delay;
private boolean dispatched;
private boolean redispatch;
public DelayedRunnable(Control control, int milliseconds)
{
this.control = control;
delay = milliseconds;
}
protected abstract void perform();
protected void prepareForDispatch()
{
}
public void run()
{
if (!control.isDisposed())
{
dispatched = false;
if (redispatch)
{
schedule();
}
else
{
perform();
}
}
}
public void schedule()
{
if (dispatched)
{
redispatch = true;
}
else
{
prepareForDispatch();
dispatched = true;
redispatch = false;
UIUtil.timerExec(delay, this);
}
}
}
}