/*******************************************************************************
* Copyright (c) 2004, 2014 Tasktop Technologies 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:
* Benjamin Pasero - initial API and implementation
* Tasktop Technologies - initial API and implementation
* Wind River Systems - Extracted from o.e.mylyn.commons and adapted for Target Explorer
*******************************************************************************/
package org.eclipse.tcf.te.ui.notifications.internal.popup;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Region;
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.Label;
import org.eclipse.swt.widgets.Monitor;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.tcf.te.ui.notifications.activator.UIPlugin;
import org.eclipse.tcf.te.ui.notifications.interfaces.IPreferenceKeys;
import org.eclipse.tcf.te.ui.notifications.interfaces.ImageConsts;
import org.eclipse.tcf.te.ui.notifications.internal.popup.AnimationUtil.FadeJob;
import org.eclipse.tcf.te.ui.notifications.internal.popup.AnimationUtil.IFadeListener;
import org.eclipse.tcf.te.ui.notifications.nls.Messages;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
/**
* A popup window with a title bar and message area for displaying notifications.
*
* @author Benjamin Pasero
* @author Mik Kersten
* @author Steffen Pingel
* @since 3.7
*/
public abstract class AbstractNotificationPopup extends Window {
private static final int TITLE_HEIGHT = 24;
private static final String LABEL_NOTIFICATION = Messages.AbstractNotificationPopup_Notification;
private static final String LABEL_JOB_CLOSE = Messages.AbstractNotificationPopup_Close_Notification_Job;
private static final int MAX_WIDTH = 400;
private static final int MIN_HEIGHT = 100;
private static final long DEFAULT_DELAY_CLOSE = 8 * 1000;
private static final int PADDING_EDGE = 5;
private long delayClose = DEFAULT_DELAY_CLOSE;
protected LocalResourceManager resources;
/* default */ GradientColors color;
private Shell shell;
private Region lastUsedRegion;
/* default */ Image lastUsedBgImage;
private boolean closed;
private final Job closeJob = new Job(LABEL_JOB_CLOSE) {
@Override
protected IStatus run(IProgressMonitor monitor) {
if (PlatformUI.isWorkbenchRunning() && PlatformUI.getWorkbench() != null && PlatformUI.getWorkbench().getDisplay() != null) {
final Display display = PlatformUI.getWorkbench().getDisplay();
if (!display.isDisposed()) {
display.asyncExec(new Runnable() {
@Override
public void run() {
Shell shell = AbstractNotificationPopup.this.getShell();
if (shell == null || shell.isDisposed()) {
return;
}
if (isMouseOver(shell)) {
scheduleAutoClose();
return;
}
AbstractNotificationPopup.this.closeFade();
}
});
}
}
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
return Status.OK_STATUS;
}
};
private final boolean respectDisplayBounds = true;
private final boolean respectMonitorBounds = true;
/* default */ FadeJob fadeJob;
private final boolean fadingEnabled;
/**
* Constructor
*
* @param parent The parent shell or <code>null</code> to create a top level shell.
* @param style The shell style.
*/
public AbstractNotificationPopup(Shell parent) {
this(parent, SWT.NO_TRIM | SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
}
/**
* Constructor
*
* @param parent The parent shell or <code>null</code> to create a top level shell.
* @param style The shell style.
*/
public AbstractNotificationPopup(Shell parent, int style) {
super(parent);
setShellStyle(style);
resources = new LocalResourceManager(JFaceResources.getResources());
color = new GradientColors(PlatformUI.getWorkbench().getDisplay(), resources);
closeJob.setSystem(true);
// Initialize the fadingEnabled flag
fadingEnabled = UIPlugin.getScopedPreferences().getBoolean(IPreferenceKeys.PREF_ENABLE_FADING);
closed = false;
}
/**
* Override to return a customized name.
*
* @return The name to be used in the title of the popup.
*/
protected String getPopupShellTitle() {
return LABEL_NOTIFICATION;
}
/**
* Override to return a customized image. Defaults to the workbench window image.
*
* @param maximumHeight The maximum height of the image.
* @return The image or <code>null</code>.
*/
protected Image getPopupShellImage(int maximumHeight) {
// always use the launching workbench window
IWorkbenchWindow[] windows = PlatformUI.getWorkbench().getWorkbenchWindows();
if (windows != null && windows.length > 0) {
IWorkbenchWindow workbenchWindow = windows[0];
if (workbenchWindow != null && !workbenchWindow.getShell().isDisposed()) {
Image image = workbenchWindow.getShell().getImage();
int diff = Integer.MAX_VALUE;
if (image != null && image.getBounds().height <= maximumHeight) {
diff = maximumHeight - image.getBounds().height;
} else {
image = null;
}
Image[] images = workbenchWindow.getShell().getImages();
if (images != null && images.length > 0) {
// find the icon that is closest in size, but not larger than maximumHeight
for (Image image2 : images) {
int newDiff = maximumHeight - image2.getBounds().height;
if (newDiff >= 0 && newDiff <= diff) {
diff = newDiff;
image = image2;
}
}
}
return image;
}
}
return null;
}
/**
* Override to populate with notifications.
*
* @param parent
*/
protected void createContentArea(Composite parent) {
// empty by default
}
/**
* Override to customize the title bar
*/
protected void createTitleArea(Composite parent) {
((GridData) parent.getLayoutData()).heightHint = TITLE_HEIGHT;
Label titleImageLabel = new Label(parent, SWT.NONE);
titleImageLabel.setImage(getPopupShellImage(TITLE_HEIGHT));
Label titleTextLabel = new Label(parent, SWT.NONE);
titleTextLabel.setText(getPopupShellTitle());
titleTextLabel.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
titleTextLabel.setForeground(getTitleForeground());
titleTextLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true));
titleTextLabel.setCursor(parent.getDisplay().getSystemCursor(SWT.CURSOR_HAND));
final Label button = new Label(parent, SWT.NONE);
button.setImage(UIPlugin.getImage(ImageConsts.NOTIFICATION_CLOSE));
button.addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
button.setImage(UIPlugin.getImage(ImageConsts.NOTIFICATION_CLOSE_HOVER));
}
@Override
public void mouseExit(MouseEvent e) {
button.setImage(UIPlugin.getImage(ImageConsts.NOTIFICATION_CLOSE));
}
});
button.addMouseListener(new MouseAdapter() {
@SuppressWarnings("synthetic-access")
@Override
public void mouseUp(MouseEvent e) {
close();
setReturnCode(CANCEL);
}
});
}
protected Color getTitleForeground() {
return color.getTitleText();
}
@Override
protected void configureShell(Shell newShell) {
super.configureShell(newShell);
shell = newShell;
newShell.setBackground(color.getBorder());
}
@Override
public void create() {
super.create();
addRegion(shell);
}
private void addRegion(Shell shell) {
Region region = new Region();
Point s = shell.getSize();
/* Add entire Shell */
region.add(0, 0, s.x, s.y);
/* Subtract Top-Left Corner */
region.subtract(0, 0, 5, 1);
region.subtract(0, 1, 3, 1);
region.subtract(0, 2, 2, 1);
region.subtract(0, 3, 1, 1);
region.subtract(0, 4, 1, 1);
/* Subtract Top-Right Corner */
region.subtract(s.x - 5, 0, 5, 1);
region.subtract(s.x - 3, 1, 3, 1);
region.subtract(s.x - 2, 2, 2, 1);
region.subtract(s.x - 1, 3, 1, 1);
region.subtract(s.x - 1, 4, 1, 1);
/* Subtract Bottom-Left Corner */
region.subtract(0, s.y, 5, 1);
region.subtract(0, s.y - 1, 3, 1);
region.subtract(0, s.y - 2, 2, 1);
region.subtract(0, s.y - 3, 1, 1);
region.subtract(0, s.y - 4, 1, 1);
/* Subtract Bottom-Right Corner */
region.subtract(s.x - 5, s.y - 0, 5, 1);
region.subtract(s.x - 3, s.y - 1, 3, 1);
region.subtract(s.x - 2, s.y - 2, 2, 1);
region.subtract(s.x - 1, s.y - 3, 1, 1);
region.subtract(s.x - 1, s.y - 4, 1, 1);
/* Dispose old first */
if (shell.getRegion() != null) {
shell.getRegion().dispose();
}
/* Apply Region */
shell.setRegion(region);
/* Remember to dispose later */
lastUsedRegion = region;
}
/* default */ boolean isMouseOver(Shell shell) {
if (PlatformUI.isWorkbenchRunning() && PlatformUI.getWorkbench() != null
&& PlatformUI.getWorkbench().getDisplay() != null && !PlatformUI.getWorkbench().getDisplay().isDisposed()) {
return shell.getBounds().contains(PlatformUI.getWorkbench().getDisplay().getCursorLocation());
}
return false;
}
@Override
public int open() {
if (shell == null || shell.isDisposed()) {
shell = null;
create();
}
constrainShellSize();
shell.setLocation(fixupDisplayBounds(shell.getSize(), shell.getLocation()));
if (fadingEnabled) {
shell.setAlpha(0);
}
shell.setVisible(true);
fadeJob = AnimationUtil.fadeIn(shell, new IFadeListener() {
@Override
public void faded(Shell shell, int alpha) {
if (shell.isDisposed()) {
return;
}
if (alpha == 255) {
scheduleAutoClose();
}
}
});
return Window.OK;
}
protected void scheduleAutoClose() {
if (delayClose > 0) {
closeJob.schedule(delayClose);
}
}
@Override
protected Control createContents(Composite parent) {
((GridLayout) parent.getLayout()).marginWidth = 1;
((GridLayout) parent.getLayout()).marginHeight = 1;
/* Outer Composite holding the controls */
final Composite outerCircle = new Composite(parent, SWT.NO_FOCUS);
outerCircle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
outerCircle.setBackgroundMode(SWT.INHERIT_FORCE);
outerCircle.addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
Rectangle clArea = outerCircle.getClientArea();
lastUsedBgImage = new Image(outerCircle.getDisplay(), clArea.width, clArea.height);
GC gc = new GC(lastUsedBgImage);
/* Gradient */
drawGradient(gc, clArea);
/* Fix Region Shape */
fixRegion(gc, clArea);
gc.dispose();
Image oldBGImage = outerCircle.getBackgroundImage();
outerCircle.setBackgroundImage(lastUsedBgImage);
if (oldBGImage != null) {
oldBGImage.dispose();
}
}
private void drawGradient(GC gc, Rectangle clArea) {
gc.setForeground(color.getGradientBegin());
gc.setBackground(color.getGradientEnd());
gc.fillGradientRectangle(clArea.x, clArea.y, clArea.width, clArea.height, true);
}
private void fixRegion(GC gc, Rectangle clArea) {
gc.setForeground(color.getBorder());
/* Fill Top Left */
gc.drawPoint(2, 0);
gc.drawPoint(3, 0);
gc.drawPoint(1, 1);
gc.drawPoint(0, 2);
gc.drawPoint(0, 3);
/* Fill Top Right */
gc.drawPoint(clArea.width - 4, 0);
gc.drawPoint(clArea.width - 3, 0);
gc.drawPoint(clArea.width - 2, 1);
gc.drawPoint(clArea.width - 1, 2);
gc.drawPoint(clArea.width - 1, 3);
/* Fill Bottom Left */
gc.drawPoint(2, clArea.height - 0);
gc.drawPoint(3, clArea.height - 0);
gc.drawPoint(1, clArea.height - 1);
gc.drawPoint(0, clArea.height - 2);
gc.drawPoint(0, clArea.height - 3);
/* Fill Bottom Right */
gc.drawPoint(clArea.width - 4, clArea.height - 0);
gc.drawPoint(clArea.width - 3, clArea.height - 0);
gc.drawPoint(clArea.width - 2, clArea.height - 1);
gc.drawPoint(clArea.width - 1, clArea.height - 2);
gc.drawPoint(clArea.width - 1, clArea.height - 3);
}
});
GridLayout layout = new GridLayout(1, false);
layout.marginWidth = 0;
layout.marginHeight = 0;
layout.verticalSpacing = 0;
outerCircle.setLayout(layout);
/* Title area containing label and close button */
final Composite titleCircle = new Composite(outerCircle, SWT.NO_FOCUS);
titleCircle.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
titleCircle.setBackgroundMode(SWT.INHERIT_FORCE);
layout = new GridLayout(4, false);
layout.marginWidth = 3;
layout.marginHeight = 0;
layout.verticalSpacing = 5;
layout.horizontalSpacing = 3;
titleCircle.setLayout(layout);
/* Create Title Area */
createTitleArea(titleCircle);
/* Outer composite to hold content controls */
Composite outerContentCircle = new Composite(outerCircle, SWT.NONE);
outerContentCircle.setBackgroundMode(SWT.INHERIT_FORCE);
layout = new GridLayout(1, false);
layout.marginWidth = 0;
layout.marginHeight = 0;
outerContentCircle.setLayout(layout);
outerContentCircle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
outerContentCircle.setBackground(outerCircle.getBackground());
/* Middle composite to show a 1px black line around the content controls */
Composite middleContentCircle = new Composite(outerContentCircle, SWT.NO_FOCUS);
middleContentCircle.setBackgroundMode(SWT.INHERIT_FORCE);
layout = new GridLayout(1, false);
layout.marginWidth = 0;
layout.marginHeight = 0;
layout.marginTop = 1;
middleContentCircle.setLayout(layout);
middleContentCircle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
middleContentCircle.setBackground(color.getBorder());
/* Inner composite containing the content controls */
Composite innerContent = new Composite(middleContentCircle, SWT.NO_FOCUS);
innerContent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
innerContent.setBackgroundMode(SWT.INHERIT_FORCE);
layout = new GridLayout(1, false);
layout.marginWidth = 0;
layout.marginHeight = 5;
layout.marginLeft = 5;
layout.marginRight = 5;
innerContent.setLayout(layout);
innerContent.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_WHITE));
/* Content Area */
createContentArea(innerContent);
return outerCircle;
}
@Override
protected void initializeBounds() {
Rectangle clArea = getPrimaryClientArea();
Point initialSize = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT);
int height = Math.max(initialSize.y, MIN_HEIGHT);
int width = Math.min(initialSize.x, MAX_WIDTH);
Point size = new Point(width, height);
shell.setLocation(clArea.width + clArea.x - size.x - PADDING_EDGE, clArea.height + clArea.y - size.y
- PADDING_EDGE);
shell.setSize(size);
}
private Rectangle getPrimaryClientArea() {
Monitor primaryMonitor = shell.getDisplay().getPrimaryMonitor();
return (primaryMonitor != null) ? primaryMonitor.getClientArea() : shell.getDisplay().getClientArea();
}
public void closeFade() {
if (fadeJob != null) {
fadeJob.cancelAndWait(false);
}
fadeJob = AnimationUtil.fadeOut(getShell(), new IFadeListener() {
@Override
public void faded(Shell shell, int alpha) {
if (shell != null && !shell.isDisposed()) {
if (alpha == 0) {
close();
} else if (isMouseOver(shell)) {
if (fadeJob != null) {
fadeJob.cancelAndWait(false);
}
fadeJob = AnimationUtil.fastFadeIn(shell, new IFadeListener() {
@Override
public void faded(Shell shell, int alpha) {
if (shell.isDisposed()) {
return;
}
if (alpha == 255) {
scheduleAutoClose();
}
}
});
}
}
}
});
}
@Override
public boolean close() {
closed = true;
if (resources != null) resources.dispose();
if (lastUsedRegion != null && !lastUsedRegion.isDisposed()) {
lastUsedRegion.dispose();
}
if (lastUsedBgImage != null && !lastUsedBgImage.isDisposed()) {
lastUsedBgImage.dispose();
}
return super.close();
}
public final boolean isClosed() {
return closed;
}
public long getDelayClose() {
return delayClose;
}
public void setDelayClose(long delayClose) {
this.delayClose = delayClose >= 0 ? delayClose : DEFAULT_DELAY_CLOSE;
}
private Point fixupDisplayBounds(Point tipSize, Point location) {
if (respectDisplayBounds) {
Rectangle bounds;
Point rightBounds = new Point(tipSize.x + location.x, tipSize.y + location.y);
if (respectMonitorBounds) {
bounds = shell.getDisplay().getPrimaryMonitor().getBounds();
} else {
bounds = getPrimaryClientArea();
}
if (!(bounds.contains(location) && bounds.contains(rightBounds))) {
if (rightBounds.x > bounds.x + bounds.width) {
location.x -= rightBounds.x - (bounds.x + bounds.width);
}
if (rightBounds.y > bounds.y + bounds.height) {
location.y -= rightBounds.y - (bounds.y + bounds.height);
}
if (location.x < bounds.x) {
location.x = bounds.x;
}
if (location.y < bounds.y) {
location.y = bounds.y;
}
}
}
return location;
}
}