package au.gov.ga.earthsci.notification.popup.ui;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.e4.ui.css.core.engine.CSSEngine;
import org.eclipse.e4.ui.css.swt.dom.WidgetElement;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Widget;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.gov.ga.earthsci.notification.INotification;
import au.gov.ga.earthsci.notification.INotificationAction;
import au.gov.ga.earthsci.notification.NotificationLevel;
import au.gov.ga.earthsci.notification.popup.Messages;
import au.gov.ga.earthsci.notification.popup.preferences.IPopupNotificationPreferences;
/**
* A popup notification widget that renders a notification in a nice fading
* popup.
* <p/>
* Implementation is inspired by the hexapixel tutorial available <a
* href="http://hexapixel.com/2009/06/30/creating-a-notification-popup-widget"
* >here</a>.
*
* @see http://hexapixel.com/2009/06/30/creating-a-notification-popup-widget
* @author James Navin (james.navin@ga.gov.au)
*/
public class PopupNotification
{
private static final int NUM_COLUMNS = 3;
private static final String POPUPS_CSS = "/css/popups.css"; //$NON-NLS-1$
// Style ID constants
private static final String DIALOG_TITLE_STYLE_CLASS = "popupDialogTitle"; //$NON-NLS-1$
private static final String DIALOG_TEXT_STYLE_CLASS = "popupDialogText"; //$NON-NLS-1$
private static final String DIALOG_IMAGE_STYLE_CLASS = "popupDialogImage"; //$NON-NLS-1$
private static final String DIALOG_LINK_STYLE_CLASS = "popupDialogLink"; //$NON-NLS-1$
private static final String DIALOG_SHELL_STYLE_CLASS = "popupDialog"; //$NON-NLS-1$
private static final String DIALOG_INFORMATION_CLASS = "information"; //$NON-NLS-1$
private static final String DIALOG_WARNING_CLASS = "warning"; //$NON-NLS-1$
private static final String DIALOG_ERROR_CLASS = "error"; //$NON-NLS-1$
/** How long each tick is when fading (in ms) */
private static final int FADE_TICK = 50;
/** The amount to increment the alpha channel by on each tick when fading in */
private static final int FADE_IN_ALPHA_STEP = 30;
/**
* The amount to decrement the alpha channel by on each tick when fading out
*/
private static final int FADE_OUT_ALPHA_STEP = 8;
/** How high the alpha value is when the popup has finished fading in */
private static final int FINAL_ALPHA = 225;
/** The amount of padding to leave around a popup */
private static final int PADDING = 2;
/** The size of the popup to create */
private static final Point POPUP_SIZE = new Point(350, 100);
/** The list of currently active popups */
private static List<PopupNotification> activePopups = new ArrayList<PopupNotification>();
/** Preferences used to control the appearance and behaviour of the popups */
private static IPopupNotificationPreferences preferences;
private static final Logger logger = LoggerFactory.getLogger(PopupNotification.class);
/** Whether the plugin-specifc CSS has been loaded */
private static AtomicBoolean cssLoaded = new AtomicBoolean(false);
/**
* Show the given notification as a popup using the current popup
* preferences and styling
*
* @param notification
* The notification to show
*/
public static void show(INotification notification, IPopupNotificationPreferences preferences)
{
PopupNotification pn = new PopupNotification(preferences);
pn.showPopupNotification(notification);
}
/** An appropriately styled shell used to create the popup */
private Shell shell;
/**
* No-arg constructor used for dependency injection
*/
PopupNotification(IPopupNotificationPreferences preferences)
{
PopupNotification.preferences = preferences;
}
/**
* Create a new popup notification for the given notification object
*
* @param notification
* The notification to use
*/
private void showPopupNotification(INotification notification)
{
if (noActiveMonitorExists())
{
return;
}
initialiseShell();
Composite inner = initialiseInner(notification);
addLevelImage(notification, inner);
addTitleLabel(notification, inner);
addCloseButton(inner);
addTextLabel(notification, inner);
addActionLinks(notification, inner);
adjustExistingPopups();
applyCSSStyling();
shell.setVisible(true);
activePopups.add(this);
fadeIn();
}
/**
* Move existing popups up the screen to make way for the new popup
*/
private void adjustExistingPopups()
{
// move other shells up
if (!activePopups.isEmpty())
{
List<PopupNotification> modifiable = new ArrayList<PopupNotification>(activePopups);
Collections.reverse(modifiable);
for (PopupNotification popup : modifiable)
{
Point currentLocation = popup.shell.getLocation();
int newY = currentLocation.y - POPUP_SIZE.y;
if (newY < 0)
{
popup.close();
}
else
{
popup.shell.setLocation(currentLocation.x, newY);
}
}
}
}
/**
* Add the notification text to the dialog
*/
private Label addTextLabel(INotification notification, Composite inner)
{
Label text = new Label(inner, SWT.WRAP);
GridData gd = new GridData(GridData.FILL_BOTH);
gd.horizontalSpan = NUM_COLUMNS;
text.setLayoutData(gd);
text.setText(notification.getText());
setCSSClass(text, DIALOG_TEXT_STYLE_CLASS, notification.getLevel());
return text;
}
/**
* Add the notification title to the dialog
*/
private CLabel addTitleLabel(INotification notification, Composite inner)
{
CLabel titleLabel = new CLabel(inner, SWT.NONE);
titleLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_CENTER));
titleLabel.setText(notification.getTitle());
setCSSClass(titleLabel, DIALOG_TITLE_STYLE_CLASS, notification.getLevel());
return titleLabel;
}
private Label addCloseButton(Composite inner)
{
final Label imageLabel = new Label(inner, SWT.FLAT);
imageLabel.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING | GridData.HORIZONTAL_ALIGN_END));
imageLabel.setImage(Icons.getCloseIcon());
imageLabel.setToolTipText(Messages.PopupNotification_CloseTooltip);
imageLabel.addMouseListener(new MouseAdapter()
{
@Override
public void mouseDown(MouseEvent e)
{
close();
}
});
imageLabel.addMouseTrackListener(new MouseTrackAdapter()
{
@Override
public void mouseEnter(MouseEvent e)
{
imageLabel.setImage(Icons.getCloseHoverIcon());
}
@Override
public void mouseExit(MouseEvent e)
{
imageLabel.setImage(Icons.getCloseIcon());
}
});
return imageLabel;
}
/**
* Add an image representing the notification level
*/
private CLabel addLevelImage(INotification notification, Composite inner)
{
CLabel imageLabel = new CLabel(inner, SWT.NONE);
imageLabel.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING | GridData.HORIZONTAL_ALIGN_BEGINNING));
imageLabel.setImage(Icons.getIcon(notification.getLevel()));
setCSSClass(imageLabel, DIALOG_IMAGE_STYLE_CLASS, notification.getLevel());
return imageLabel;
}
/**
* Add a link for each notification action present on the notification
*/
private List<Link> addActionLinks(INotification notification, Composite inner)
{
List<Link> result = new ArrayList<Link>();
if (notification.getActions() == null || notification.getActions().length == 0)
{
return result;
}
for (final INotificationAction action : notification.getActions())
{
Link link = new Link(inner, SWT.NONE);
GridData gd = new GridData(GridData.FILL_BOTH);
gd.horizontalSpan = NUM_COLUMNS;
link.setLayoutData(gd);
link.setToolTipText(action.getTooltip());
link.setText("- <a>" + action.getText() + "</a>"); //$NON-NLS-1$ //$NON-NLS-2$
link.addSelectionListener(new SelectionListener()
{
@Override
public void widgetSelected(SelectionEvent e)
{
action.run();
}
@Override
public void widgetDefaultSelected(SelectionEvent e)
{
action.run();
}
});
setCSSClass(link, DIALOG_LINK_STYLE_CLASS, notification.getLevel());
result.add(link);
}
return result;
}
/**
* Create the shell that will be used to display the dialog
*/
private void initialiseShell()
{
shell = new Shell(getRootShell(Display.getDefault().getActiveShell()), SWT.NO_FOCUS | SWT.NO_TRIM);
shell.setLayout(new FillLayout());
shell.setBackgroundMode(SWT.INHERIT_DEFAULT);
shell.addListener(SWT.Dispose, new Listener()
{
@Override
public void handleEvent(Event event)
{
activePopups.remove(PopupNotification.this);
}
});
shell.setSize(POPUP_SIZE);
shell.setAlpha(0);
Rectangle clientArea = Display.getDefault().getActiveShell().getMonitor().getClientArea();
int startX = clientArea.x + clientArea.width - POPUP_SIZE.x - PADDING;
int startY = clientArea.y + clientArea.height - POPUP_SIZE.y - PADDING;
shell.setLocation(startX, startY);
}
/**
* Initialise the grid that will hold the dialog components
*/
private Composite initialiseInner(INotification notification)
{
final Composite inner = new Composite(shell, SWT.NONE);
GridLayout gl = new GridLayout(NUM_COLUMNS, false);
gl.marginLeft = 5;
gl.marginTop = 0;
gl.marginRight = 5;
gl.marginBottom = 5;
inner.setLayout(gl);
setCSSClass(inner, DIALOG_SHELL_STYLE_CLASS, notification.getLevel());
return inner;
}
/**
* Setup the CSS class attribute for the provided widget, adding the
* appropriate level class
*/
private void setCSSClass(Widget widget, String mainClass, NotificationLevel notificationLevel)
{
WidgetElement.setCSSClass(widget, mainClass + " " + getLevelClass(notificationLevel)); //$NON-NLS-1$
}
/**
* @return The CSS class to use for the provided notification level
*/
private String getLevelClass(NotificationLevel level)
{
return level == NotificationLevel.INFORMATION ? DIALOG_INFORMATION_CLASS : level == NotificationLevel.WARNING
? DIALOG_WARNING_CLASS : DIALOG_ERROR_CLASS;
}
/**
* Apply the styling to the shell and all of its children
*/
private void applyCSSStyling()
{
CSSEngine cssEngine = WidgetElement.getEngine(Display.getCurrent());
if (cssEngine == null)
{
//this occurs when the display hasn't yet been created
logger.debug("Error finding CSS engine for the current display"); //$NON-NLS-1$
return;
}
if (!cssLoaded.get())
{
try
{
cssEngine.parseStyleSheet(getClass().getResourceAsStream(POPUPS_CSS));
}
catch (Exception e)
{
logger.error("Exception occurred while loading popup notification CSS", e); //$NON-NLS-1$
}
cssLoaded.set(true);
}
cssEngine.applyStyles(shell, true);
}
/**
* @return <code>true</code> if there is no active shell or monitor in the
* current display hierachy
*/
private boolean noActiveMonitorExists()
{
return Display.getDefault().getActiveShell() == null
|| Display.getDefault().getActiveShell().getMonitor() == null;
}
/**
* Helper method that finds the root shell of the current shell.
* <p/>
* Used in the case where the active shell is e.g. a dialog box
*/
private static Shell getRootShell(Composite s)
{
if (s.getParent() == null)
{
return (Shell) s;
}
return getRootShell(s.getParent());
}
/**
* Fade in this popup over the configured duration
*/
private void fadeIn()
{
Runnable run = new Runnable()
{
@Override
public void run()
{
try
{
if (shell == null || shell.isDisposed())
{
return;
}
int popupAlpha = shell.getAlpha();
popupAlpha += FADE_IN_ALPHA_STEP;
if (popupAlpha > FINAL_ALPHA)
{
shell.setAlpha(FINAL_ALPHA);
startDisplayTimer();
return;
}
shell.setAlpha(popupAlpha);
Display.getDefault().timerExec(FADE_TICK, this);
}
catch (Exception e)
{
logger.error("Error fading in", e); //$NON-NLS-1$
}
}
};
Display.getDefault().timerExec(FADE_TICK, run);
}
/**
* Start the display timer for this popup
*/
private void startDisplayTimer()
{
Runnable run = new Runnable()
{
@Override
public void run()
{
try
{
if (shell == null || shell.isDisposed())
{
return;
}
fadeOut();
}
catch (Exception e)
{
logger.error("Error running display timer", e); //$NON-NLS-1$
}
}
};
Display.getDefault().timerExec(preferences.getDisplayDuration(), run);
}
/**
* Fade out this popup over the configured duration
*/
private void fadeOut()
{
final Runnable run = new Runnable()
{
@Override
public void run()
{
try
{
if (shell == null || shell.isDisposed())
{
return;
}
int popupAlpha = shell.getAlpha();
popupAlpha -= FADE_OUT_ALPHA_STEP;
if (popupAlpha <= 0)
{
shell.setAlpha(0);
close();
return;
}
shell.setAlpha(popupAlpha);
Display.getDefault().timerExec(FADE_TICK, this);
}
catch (Exception e)
{
logger.error("Error fading out", e); //$NON-NLS-1$
}
}
};
Display.getDefault().timerExec(FADE_TICK, run);
}
/**
* Close the popup
*/
public void close()
{
dispose();
activePopups.remove(this);
}
/**
* Perform required cleanup
*/
public void dispose()
{
if (shell != null)
{
shell.dispose();
}
}
}