package au.gov.ga.earthsci.notification.ui;
import java.lang.reflect.InvocationTargetException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.ui.model.application.ui.basic.MPart;
import org.eclipse.e4.ui.model.application.ui.menu.MMenu;
import org.eclipse.e4.ui.model.application.ui.menu.MMenuElement;
import org.eclipse.e4.ui.model.application.ui.menu.MMenuItem;
import org.eclipse.e4.ui.services.IServiceConstants;
import org.eclipse.e4.ui.workbench.swt.internal.copy.FilteredTree;
import org.eclipse.e4.ui.workbench.swt.internal.copy.PatternFilter;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
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.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import au.gov.ga.earthsci.common.ui.dialogs.StackTraceDialog;
import au.gov.ga.earthsci.notification.INotification;
import au.gov.ga.earthsci.notification.NotificationCategory;
import au.gov.ga.earthsci.notification.NotificationLevel;
import au.gov.ga.earthsci.notification.ui.handlers.GroupByHandler;
/**
* A view that displays a filtered and/or grouped table of historic
* {@link INotification}s
* <p/>
* The model for this view is maintained by an injected
* {@link NotificationPartReceiver} instance.
*
* @author James Navin (james.navin@ga.gov.au)
*/
public class NotificationPart
{
private static final int TITLE_COLUMN_INDEX = 0;
private static final int DESCRIPTION_COLUMN_INDEX = 1;
private static final int CATEGORY_COLUMN_INDEX = 2;
private static final int CREATED_COLUMN_INDEX = 3;
private static final int ACKNOWLEDGED_COLUMN_INDEX = 4;
private static final int ASCENDING = 1;
private static final int DESCENDING = -1;
private static ImageRegistry imageRegistry;
/**
* A simple enumeration of possible groupings for content in the
* {@link NotificationPart}
*/
public enum Grouping
{
NONE,
LEVEL,
CATEGORY
}
private Grouping groupBy = Grouping.NONE;
private NotificationPartReceiver receiver;
private Tree tree;
private FilteredTree filteredTree;
private List<Object> root = new ArrayList<Object>();
// Table columns
private int titleOrder = ASCENDING;
private TreeColumn titleColumn;
private int descriptionOrder = ASCENDING;
private TreeColumn descriptionColumn;
private int categoryOrder = ASCENDING;
private TreeColumn categoryColumn;
private int createdOrder = DESCENDING;
private TreeColumn createdColumn;
private int acknowledgedOrder = DESCENDING;
private TreeColumn acknowledgedColumn;
@Inject
@Optional
@Named(IServiceConstants.ACTIVE_SHELL)
private Shell shell;
@Inject
private IEclipseContext context;
/**
* Initialise this view with the given parent component
*/
@PostConstruct
public void init(Composite parent, MPart part, NotificationPartReceiver receiver)
{
context.set(NotificationPart.class, this);
GridLayout layout = new GridLayout(1, false);
parent.setLayout(layout);
createViewer(parent);
initialiseCorrectGrouping(part);
setReceiver(receiver);
}
@PreDestroy
public void dispose()
{
context.remove(NotificationPart.class);
}
/**
* Set the receiver that this view should listen to for notifications
*/
private void setReceiver(NotificationPartReceiver receiver)
{
this.receiver = receiver;
this.receiver.setView(this);
reloadNotificationTree(false);
}
/**
* Set the grouping to use for this view.
* <p/>
* Note that this will usually cause the view to reload all historic
* notifications from the attached receiver.
*/
public void setGrouping(Grouping g)
{
setGrouping(g, true);
}
public void setGrouping(Grouping g, boolean showProgress)
{
if (g == groupBy)
{
return;
}
this.groupBy = g;
reloadNotificationTree(showProgress);
}
private void createViewer(Composite parent)
{
PatternFilter filter = new PatternFilter()
{
@Override
protected boolean isLeafMatch(Viewer viewer, Object element)
{
if (element instanceof INotification)
{
INotification n = (INotification) element;
return wordMatches(n.getLevel().name()) || wordMatches(n.getTitle()) || wordMatches(n.getText())
|| wordMatches(n.getCategory().getLabel());
}
return false;
}
};
filter.setIncludeLeadingWildcard(true);
filteredTree = new FilteredTree(parent, SWT.FULL_SELECTION, filter, true);
if (filteredTree.getFilterControl() != null)
{
Composite filterComposite = filteredTree.getFilterControl().getParent();
GridData gd = (GridData) filterComposite.getLayoutData();
gd.verticalIndent = 2;
gd.horizontalIndent = 1;
}
filteredTree.setLayoutData(new GridData(GridData.FILL_BOTH));
filteredTree.setInitialText(Messages.NotificationView_FilterTextBoxLabel);
tree = filteredTree.getViewer().getTree();
tree.setLinesVisible(true);
tree.setHeaderVisible(true);
createColumns(tree);
filteredTree.getViewer().setAutoExpandLevel(2);
filteredTree.getViewer().setContentProvider(new NotificationViewContentProvider(this));
filteredTree.getViewer().setLabelProvider(new NotificationViewTreeLabelProvider());
filteredTree.getViewer().setInput(this);
filteredTree.getViewer().getTree().addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetDefaultSelected(SelectionEvent e)
{
Object data = e.item.getData();
if (data instanceof INotification)
{
INotification notification = (INotification) data;
if (notification.getThrowable() != null)
{
IStatus status =
new Status(notification.getLevel().getStatusSeverity(), Activator.PLUGIN_ID,
notification.getThrowable().getLocalizedMessage(), notification.getThrowable());
StackTraceDialog.openError(shell, notification.getTitle(), notification.getText(), status);
}
else
{
IStatus status =
new Status(notification.getLevel().getStatusSeverity(), Activator.PLUGIN_ID,
notification.getText());
ErrorDialog.openError(shell, notification.getTitle(), null, status);
}
}
}
});
}
private void createColumns(Tree tree)
{
String[] titles =
{ Messages.NotificationView_TitleColumnLabel, Messages.NotificationView_DescriptionColumnLabel,
Messages.NotificationView_CategoryColumnLabel, Messages.NotificationView_CreateColumnLabel,
Messages.NotificationView_AcknowledgedColumnLabel };
int[] widths = { 300, 300, 100, 100, 100 };
titleColumn = createTreeColumn(titles[TITLE_COLUMN_INDEX], widths[TITLE_COLUMN_INDEX]);
titleColumn.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
titleOrder *= -1;
filteredTree.getViewer().setComparator(new ViewerComparator()
{
@Override
public int compare(Viewer viewer, Object e1, Object e2)
{
if (!(e1 instanceof INotification) || !(e2 instanceof INotification))
{
return 0;
}
return String.CASE_INSENSITIVE_ORDER.compare(((INotification) e1).getTitle(),
((INotification) e2).getTitle()) * titleOrder;
}
});
setColumnSorting(TITLE_COLUMN_INDEX, titleOrder);
}
});
descriptionColumn = createTreeColumn(titles[DESCRIPTION_COLUMN_INDEX], widths[DESCRIPTION_COLUMN_INDEX]);
descriptionColumn.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
descriptionOrder *= -1;
filteredTree.getViewer().setComparator(new ViewerComparator()
{
@Override
public int compare(Viewer viewer, Object e1, Object e2)
{
if (!(e1 instanceof INotification) || !(e2 instanceof INotification))
{
return 0;
}
return String.CASE_INSENSITIVE_ORDER.compare(((INotification) e1).getText(),
((INotification) e2).getText()) * descriptionOrder;
}
});
setColumnSorting(DESCRIPTION_COLUMN_INDEX, descriptionOrder);
}
});
categoryColumn = createTreeColumn(titles[CATEGORY_COLUMN_INDEX], widths[CATEGORY_COLUMN_INDEX]);
categoryColumn.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
categoryOrder *= -1;
filteredTree.getViewer().setComparator(new ViewerComparator()
{
@Override
public int compare(Viewer viewer, Object e1, Object e2)
{
if (!(e1 instanceof INotification) || !(e2 instanceof INotification))
{
return 0;
}
return String.CASE_INSENSITIVE_ORDER.compare(((INotification) e1).getCategory().getLabel(),
((INotification) e2).getCategory().getLabel()) * categoryOrder;
}
});
setColumnSorting(CATEGORY_COLUMN_INDEX, categoryOrder);
}
});
createdColumn = createTreeColumn(titles[CREATED_COLUMN_INDEX], widths[CREATED_COLUMN_INDEX]);
createdColumn.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
createdOrder *= -1;
filteredTree.getViewer().setComparator(new ViewerComparator()
{
@Override
public int compare(Viewer viewer, Object e1, Object e2)
{
if (!(e1 instanceof INotification) || !(e2 instanceof INotification))
{
return 0;
}
return ((INotification) e1).getCreationTimestamp().compareTo(
((INotification) e2).getCreationTimestamp())
* createdOrder;
}
});
setColumnSorting(CREATED_COLUMN_INDEX, createdOrder);
}
});
acknowledgedColumn = createTreeColumn(titles[ACKNOWLEDGED_COLUMN_INDEX], widths[ACKNOWLEDGED_COLUMN_INDEX]);
acknowledgedColumn.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
acknowledgedOrder *= -1;
filteredTree.getViewer().setComparator(new ViewerComparator()
{
@Override
public int compare(Viewer viewer, Object e1, Object e2)
{
if (!(e1 instanceof INotification) || !(e2 instanceof INotification))
{
return 0;
}
INotification n1 = (INotification) e1;
INotification n2 = (INotification) e2;
if (n1.getAcknowledgementTimestamp() == null && n2.getAcknowledgementTimestamp() == null)
{
return 0;
}
if (n1.getAcknowledgementTimestamp() == null && n2.getAcknowledgementTimestamp() != null)
{
return ASCENDING;
}
if (n1.getAcknowledgementTimestamp() != null && n2.getAcknowledgementTimestamp() == null)
{
return DESCENDING;
}
return n1.getAcknowledgementTimestamp().compareTo(n2.getAcknowledgementTimestamp())
* acknowledgedOrder;
}
});
setColumnSorting(ACKNOWLEDGED_COLUMN_INDEX, acknowledgedOrder);
}
});
}
private TreeColumn createTreeColumn(String title, int width)
{
TreeColumn c = new TreeColumn(tree, SWT.LEFT);
c.setText(title);
c.setWidth(width);
return c;
}
private void setColumnSorting(int index, int order)
{
tree.setSortColumn(tree.getColumn(index));
tree.setSortDirection(order == ASCENDING ? SWT.UP : SWT.DOWN);
}
/**
* Re-build the notification tree from the notification list on the attached
* receiver using the current grouping/filtering settings.
*/
public void reloadNotificationTree(final boolean showProgress)
{
if (receiver == null)
{
return;
}
final IRunnableWithProgress op = new IRunnableWithProgress()
{
@Override
public void run(IProgressMonitor monitor)
{
monitor.beginTask(Messages.NotificationView_ProgressDescription, IProgressMonitor.UNKNOWN);
root.clear();
// Build a flat list - easy!
if (groupBy == Grouping.NONE)
{
root.addAll(receiver.getNotifications());
}
else if (groupBy == Grouping.LEVEL)
{
Map<NotificationLevel, Group> groups =
new EnumMap<NotificationLevel, Group>(NotificationLevel.class);
for (INotification n : receiver.getNotifications())
{
Group g = groups.get(n.getLevel());
if (g == null)
{
g = new Group(n.getLevel(), n.getLevel().getLabel());
groups.put(n.getLevel(), g);
root.add(g);
}
g.children.add(n);
}
Collections.sort(root, levelGroupComparator);
}
else if (groupBy == Grouping.CATEGORY)
{
Map<NotificationCategory, Group> groups = new HashMap<NotificationCategory, Group>();
for (INotification n : receiver.getNotifications())
{
Group g = groups.get(n.getCategory());
if (g == null)
{
g = new Group(n.getCategory(), n.getCategory().getLabel());
groups.put(n.getCategory(), g);
root.add(g);
}
g.children.add(n);
}
Collections.sort(root, categoryGroupComparator);
}
}
};
Display.getDefault().asyncExec(new Runnable()
{
@Override
public void run()
{
try
{
if (showProgress)
{
ProgressMonitorDialog pmd = new ProgressMonitorDialog(tree.getShell());
pmd.run(true, true, op);
}
else
{
op.run(new NullProgressMonitor());
}
}
catch (InvocationTargetException e)
{
// do nothing
}
catch (InterruptedException e)
{
// do nothing
}
finally
{
asyncRefresh();
}
}
});
}
/**
* Refresh the view upon receiving a new (given) notification
* <p/>
* This is intended to be only executed from the associated receiver.
*/
void refresh(INotification n)
{
// Insert the new notification into the correct place in the tree model
switch (groupBy)
{
case NONE:
{
root.add(n);
break;
}
case LEVEL:
{
Group targetGroup = null;
for (int i = 0; i < root.size(); i++)
{
Group g = (Group) root.get(i);
if (g.grouping.equals(n.getLevel()))
{
targetGroup = g;
break;
}
}
if (targetGroup == null)
{
targetGroup = new Group(n.getLevel(), n.getLevel().getLabel());
root.add(targetGroup);
Collections.sort(root, levelGroupComparator);
}
targetGroup.children.add(n);
break;
}
case CATEGORY:
{
Group targetGroup = null;
for (int i = 0; i < root.size(); i++)
{
Group g = (Group) root.get(i);
if (g.grouping.equals(n.getCategory()))
{
targetGroup = g;
break;
}
}
if (targetGroup == null)
{
targetGroup = new Group(n.getCategory(), n.getCategory().getLabel());
root.add(targetGroup);
Collections.sort(root, categoryGroupComparator);
}
targetGroup.children.add(n);
break;
}
}
// Refresh the tree
Display.getDefault().asyncExec(new Runnable()
{
@Override
public void run()
{
filteredTree.getViewer().refresh();
}
});
}
/**
* Refresh the tree view on the appropriate display thread
*/
private void asyncRefresh()
{
if (tree.isDisposed())
{
return;
}
Display display = tree.getDisplay();
if (display == null)
{
return;
}
display.asyncExec(new Runnable()
{
@Override
public void run()
{
if (!tree.isDisposed())
{
TreeViewer viewer = filteredTree.getViewer();
viewer.refresh();
viewer.expandToLevel(2);
}
}
});
}
/**
* Initialise the grouping as per the persisted menu selection on startup
*/
private void initialiseCorrectGrouping(MPart part)
{
// Find the active grouping
String selectedId = null;
for (MMenu menu : part.getMenus())
{
if (menu.getTags().contains("ViewMenu")) //$NON-NLS-1$
{
for (MMenuElement element : menu.getChildren())
{
if (!element.getElementId().equals("au.gov.ga.earthsci.notification.part.NotificationPart.group")) //$NON-NLS-1$
{
continue;
}
for (MMenuElement child : ((MMenu) element).getChildren())
{
if (((MMenuItem) child).isSelected())
{
selectedId = child.getElementId();
}
}
}
}
}
Grouping grouping = GroupByHandler.getGroupingForMenuItemId(selectedId);
setGrouping(grouping, false);
}
/**
* @return the imageRegistry
*/
public static ImageRegistry getImageRegistry()
{
if (imageRegistry == null)
{
ImageRegistry imageRegistry = new ImageRegistry();
imageRegistry.put(NotificationLevel.INFORMATION.name(),
ImageDescriptor.createFromURL(NotificationPart.class.getResource("/icons/info.gif"))); //$NON-NLS-1$
imageRegistry.put(NotificationLevel.WARNING.name(),
ImageDescriptor.createFromURL(NotificationPart.class.getResource("/icons/warning.gif"))); //$NON-NLS-1$
imageRegistry.put(NotificationLevel.ERROR.name(),
ImageDescriptor.createFromURL(NotificationPart.class.getResource("/icons/error.gif"))); //$NON-NLS-1$
NotificationPart.imageRegistry = imageRegistry;
}
return imageRegistry;
}
/**
* A simple container class that represents a group of notifications
* <p/>
* Used to construct the simple 2-level tree used for this view
*/
private static class Group
{
private String label;
private Object grouping;
private List<INotification> children = new ArrayList<INotification>();
public Group(Object grouping, String label)
{
this.label = label;
this.grouping = grouping;
}
}
/** A comparator for ordering level groups in the view */
private static final Comparator<Object> levelGroupComparator = new Comparator<Object>()
{
@Override
public int compare(Object o1, Object o2)
{
return NotificationLevel.SEVERITY_DESCENDING.compare((NotificationLevel) ((Group) o1).grouping,
(NotificationLevel) ((Group) o2).grouping);
}
};
/** A comparator for ordering category groups in the view */
private static final Comparator<Object> categoryGroupComparator = new Comparator<Object>()
{
@Override
public int compare(Object o1, Object o2)
{
return String.CASE_INSENSITIVE_ORDER.compare(((Group) o1).label, ((Group) o2).label);
}
};
/**
* A label provider that renders appropriate labels for the notification
* tree
*/
private static class NotificationViewTreeLabelProvider extends LabelProvider implements ITableLabelProvider
{
private static final String DATE_FORMAT = "dd MMM yyyy HH:mm:ss"; //$NON-NLS-1$
@Override
public Image getColumnImage(Object element, int columnIndex)
{
if (element instanceof INotification && columnIndex == TITLE_COLUMN_INDEX)
{
return getImageRegistry().get(((INotification) element).getLevel().name());
}
return null;
}
@Override
public String getColumnText(Object element, int columnIndex)
{
if (element instanceof INotification)
{
INotification n = (INotification) element;
switch (columnIndex)
{
case TITLE_COLUMN_INDEX:
return n.getTitle();
case DESCRIPTION_COLUMN_INDEX:
return n.getText();
case CATEGORY_COLUMN_INDEX:
return n.getCategory().getLabel();
case CREATED_COLUMN_INDEX:
return new SimpleDateFormat(DATE_FORMAT).format(n.getCreationTimestamp());
case ACKNOWLEDGED_COLUMN_INDEX:
return n.isAcknowledged() ? new SimpleDateFormat(DATE_FORMAT).format(n
.getAcknowledgementTimestamp()) : ""; //$NON-NLS-1$
}
}
if (element instanceof Group && columnIndex == 0)
{
return ((Group) element).label;
}
return ""; //$NON-NLS-1$
}
}
/**
* An {@link ITreeContentProvider} that understands grouping within the
* notification view
*/
private static class NotificationViewContentProvider implements ITreeContentProvider
{
private final NotificationPart view;
public NotificationViewContentProvider(NotificationPart view)
{
this.view = view;
}
@Override
public void dispose()
{
// DO nothing
}
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput)
{
// Do nothing
}
@Override
public Object[] getElements(Object inputElement)
{
return view.root.toArray();
}
@SuppressWarnings("unchecked")
@Override
public Object[] getChildren(Object parentElement)
{
if (parentElement instanceof Group)
{
return ((Group) parentElement).children.toArray();
}
else if (parentElement instanceof List)
{
return ((List<Object>) parentElement).toArray();
}
return null;
}
@Override
public Object getParent(Object element)
{
if (element instanceof List)
{
return null;
}
if (element instanceof Group)
{
return view.root;
}
if (element instanceof INotification && view.groupBy == Grouping.NONE)
{
return view.root;
}
INotification n = (INotification) element;
for (Object o : view.root)
{
if (view.groupBy == Grouping.LEVEL && ((Group) o).grouping.equals(n.getLevel()))
{
return o;
}
if (view.groupBy == Grouping.CATEGORY && ((Group) o).grouping.equals(n.getCategory()))
{
return o;
}
}
return null;
}
@Override
public boolean hasChildren(Object element)
{
return !(element instanceof INotification);
}
}
}