/******************************************************************************* * Copyright (c) 2011, 2012 Anton Gorenkov * 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: * Anton Gorenkov - initial API and implementation *******************************************************************************/ package org.eclipse.cdt.testsrunner.internal.ui.view; import java.io.File; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; import java.util.Set; import java.util.TreeSet; import org.eclipse.cdt.testsrunner.internal.model.TestingSessionsManager; import org.eclipse.cdt.testsrunner.internal.ui.view.actions.CopySelectedMessagesAction; import org.eclipse.cdt.testsrunner.internal.ui.view.actions.OpenInEditorAction; import org.eclipse.cdt.testsrunner.model.IModelVisitor; import org.eclipse.cdt.testsrunner.model.ITestCase; import org.eclipse.cdt.testsrunner.model.ITestItem; import org.eclipse.cdt.testsrunner.model.ITestLocation; import org.eclipse.cdt.testsrunner.model.ITestMessage; import org.eclipse.cdt.testsrunner.model.ITestMessage.Level; import org.eclipse.cdt.testsrunner.model.ITestSuite; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.viewers.IOpenListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.OpenEvent; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Menu; import org.eclipse.ui.IActionBars; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.IViewSite; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.actions.ActionFactory; /** * Shows the messages for the currently selected items in tests hierarchy (test * suites or test cases). */ public class MessagesViewer { /** * Enumeration of all possible message filter actions by level. */ public enum LevelFilter { Info(ISharedImages.IMG_OBJS_INFO_TSK, ITestMessage.Level.Info, ITestMessage.Level.Message), Warning(ISharedImages.IMG_OBJS_WARN_TSK, ITestMessage.Level.Warning), Error(ISharedImages.IMG_OBJS_ERROR_TSK, ITestMessage.Level.Error, ITestMessage.Level.FatalError, ITestMessage.Level.Exception); private String imageId; private ITestMessage.Level [] includedLevels; LevelFilter(String imageId, ITestMessage.Level... includedLevels) { this.imageId = imageId; this.includedLevels = includedLevels; } /** * The shared image ID corresponding to the message level filter action. * * @return shared image ID */ public String getImageId() { return imageId; } /** * The message levels that should be shown if current message level * filter action is set. * * @return array of message levels */ public ITestMessage.Level [] getLevels() { return includedLevels; } /** * Checks whether the specified message level should be shown if current * message level filter action is set. * * @param messageLevel message level to search * @return <code>true</code> if found */ public boolean isIncluded(ITestMessage.Level messageLevel) { for (ITestMessage.Level currLevel : includedLevels) { if (currLevel.equals(messageLevel)) { return true; } } return false; } } /** * The content provider for the test messages viewer. */ private class MessagesContentProvider implements IStructuredContentProvider { /** * Utility class: recursively collects all the messages of the specified * test item. */ class MessagesCollector implements IModelVisitor { /** Collected test messages. */ Collection<ITestMessage> collectedTestMessages; /** * Specifies whether gathering should be done. It is used to skip * the messages of the passed tests if they should not be shown. */ boolean collect = true; MessagesCollector(Collection<ITestMessage> testMessages) { this.collectedTestMessages = testMessages; } @Override public void visit(ITestMessage testMessage) { if (collect) { collectedTestMessages.add(testMessage); } } @Override public void visit(ITestCase testCase) { collect = !showFailedOnly || testCase.getStatus().isError(); } @Override public void visit(ITestSuite testSuite) {} @Override public void leave(ITestSuite testSuite) {} @Override public void leave(ITestCase testCase) {} @Override public void leave(ITestMessage testMessage) {} } /** Test messages to show in the viewer. */ ITestMessage[] testMessages; @Override public void inputChanged(Viewer v, Object oldInput, Object newInput) { if (newInput != null) { collectMessages((ITestItem[]) newInput); } else { testMessages = new ITestMessage[0]; } } @Override public void dispose() { } @Override public Object[] getElements(Object parent) { return testMessages; } /** * Creates a messages set with a custom comparator. It is used for the * ordered messages showing. * * @return set to store the test messages */ private TreeSet<ITestMessage> createMessagesSet() { return new TreeSet<ITestMessage>(new Comparator<ITestMessage>() { @Override public int compare(ITestMessage message1, ITestMessage message2) { // Compare messages by location ITestLocation location1 = message1.getLocation(); ITestLocation location2 = message2.getLocation(); if (location1 != null && location2 != null) { // Compare by file name String file1 = location1.getFile(); String file2 = location2.getFile(); int fileResult = file1.compareTo(file2); if (fileResult != 0) { return fileResult; } else { // Compare by line number int line1 = location1.getLine(); int line2 = location2.getLine(); if (line1 < line2) { return -1; } else if (line1 > line2) { return 1; } } } else if (location1 == null && location2 != null) { return -1; } else if (location1 != null && location2 == null) { return 1; } // Compare by message text String text1 = message1.getText(); String text2 = message2.getText(); return text1.compareTo(text2); } }); } /** * Creates a list to store the test messages. It is used for the * unordered messages showing. * * @return list to store the test messages */ private ArrayList<ITestMessage> createMessagesList() { return new ArrayList<ITestMessage>(); } /** * Creates a collection to store the test messages depending on whether * ordering is required. * * @return collection to store the test messages */ private Collection<ITestMessage> createMessagesCollection() { return orderingMode ? createMessagesSet() : createMessagesList(); } /** * Run messages collecting for the specified test items. * * @param testItems test items array */ private void collectMessages(ITestItem[] testItems) { Collection<ITestMessage> testMessagesCollection = createMessagesCollection(); for (ITestItem testItem : testItems) { testItem.visit(new MessagesCollector(testMessagesCollection)); } testMessages = testMessagesCollection.toArray(new ITestMessage[testMessagesCollection.size()]); } } /** * The label provider for the test messages viewer. */ private class MessagesLabelProvider extends LabelProvider implements ITableLabelProvider { /** * Returns the full (file path) or short (file name only) file path view * depending on the filter set. * * @param location test object location * @return file path */ private String getLocationFile(ITestLocation location) { String filePath = location.getFile(); if (showFileNameOnly) { return new File(filePath).getName(); } else { return filePath; } } @Override public String getColumnText(Object obj, int index) { ITestMessage message = (ITestMessage)obj; ITestLocation location = message.getLocation(); String locationString = ""; //$NON-NLS-1$ if (location != null) { locationString = MessageFormat.format( UIViewMessages.MessagesViewer_location_format, new Object[] { getLocationFile(location), location.getLine() } ); } return MessageFormat.format(UIViewMessages.MessagesViewer_message_format, locationString, message.getLevel(), message.getText() ); } @Override public Image getColumnImage(Object obj, int index) { return getImage(obj); } @Override public Image getImage(Object obj) { Level level = ((ITestMessage)obj).getLevel(); String imageId = ISharedImages.IMG_OBJ_ELEMENT; for (LevelFilter levelFilter : LevelFilter.values()) { if (levelFilter.isIncluded(level)) { imageId = levelFilter.getImageId(); break; } } return PlatformUI.getWorkbench().getSharedImages().getImage(imageId); } } /** * Filters the required test messages by level. */ private class MessageLevelFilter extends ViewerFilter { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { return acceptedMessageLevels.contains(((ITestMessage)element).getLevel()); } } /** Main widget. */ private TableViewer tableViewer; private IViewSite viewSite; // Context menu actions private OpenInEditorAction openInEditorAction; private Action copyAction; /** Specifies whether only messages for failed tests should be shown. */ private boolean showFailedOnly = false; /** * Specifies whether only file names should be shown (instead of full file * paths). */ private boolean showFileNameOnly = false; /** The set of message level to show the messages with. */ private Set<ITestMessage.Level> acceptedMessageLevels = new HashSet<ITestMessage.Level>(); /** Specifies whether test messages ordering is on or off. */ private boolean orderingMode = false; public MessagesViewer(Composite parent, TestingSessionsManager sessionsManager, IWorkbench workbench, IViewSite viewSite, Clipboard clipboard) { this.viewSite = viewSite; tableViewer = new TableViewer(parent, SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL); tableViewer.setLabelProvider(new MessagesLabelProvider()); tableViewer.setContentProvider(new MessagesContentProvider()); tableViewer.addFilter(new MessageLevelFilter()); initContextMenu(viewSite, sessionsManager, workbench, clipboard); tableViewer.addOpenListener(new IOpenListener() { @Override public void open(OpenEvent event) { openInEditorAction.run(); } }); } /** * Initializes the viewer context menu. * * @param viewSite view * @param sessionsManager testing sessions manager * @param workbench workbench * @param clipboard clipboard */ private void initContextMenu(IViewSite viewSite, TestingSessionsManager sessionsManager, IWorkbench workbench, Clipboard clipboard) { openInEditorAction = new OpenInEditorAction(tableViewer, sessionsManager, workbench); copyAction = new CopySelectedMessagesAction(tableViewer, clipboard); MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$ menuMgr.addMenuListener(new IMenuListener() { @Override public void menuAboutToShow(IMenuManager manager) { handleMenuAboutToShow(manager); } }); viewSite.registerContextMenu(menuMgr, tableViewer); Menu menu = menuMgr.createContextMenu(tableViewer.getTable()); tableViewer.getTable().setMenu(menu); menuMgr.add(openInEditorAction); menuMgr.add(copyAction); configureCopy(); } /** * Configures the view copy action which should be run on CTRL+C. We have to * track widget focus to select the actual action because we have a few * widgets that should provide copy action (at least tests hierarchy viewer * and messages viewer). */ private void configureCopy() { getTableViewer().getTable().addFocusListener(new FocusListener() { IAction viewCopyHandler; @Override public void focusLost(FocusEvent e) { if (viewCopyHandler != null) { switchTo(viewCopyHandler); } } @Override public void focusGained(FocusEvent e) { switchTo(copyAction); } private void switchTo(IAction copyAction) { IActionBars actionBars = viewSite.getActionBars(); viewCopyHandler = actionBars.getGlobalActionHandler(ActionFactory.COPY.getId()); actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), copyAction); actionBars.updateActionBars(); } }); } /** * Handles the context menu showing. * * @param manager context menu manager */ private void handleMenuAboutToShow(IMenuManager manager) { ISelection selection = tableViewer.getSelection(); openInEditorAction.setEnabled(!selection.isEmpty()); copyAction.setEnabled(!selection.isEmpty()); } /** * Provides access to the main widget of the messages viewer. * * @return main widget of the messages viewer */ public TableViewer getTableViewer() { return tableViewer; } /** * Sets the test items for which the messages should be shown. * * @param testItems test items array */ public void showItemsMessages(ITestItem[] testItems) { tableViewer.setInput(testItems); } /** * Forces the messages recollecting. It is used after message filters * change. */ private void forceRecollectMessages() { // NOTE: Set input again makes content provider to recollect messages (with filters applied) tableViewer.setInput(tableViewer.getInput()); } /** * Returns whether the messages only for the failed tests should be shown. * * @return filter state */ public boolean getShowFailedOnly() { return showFailedOnly; } /** * Sets whether the messages only for the failed tests should be shown. * * @param showFailedOnly new filter state */ public void setShowFailedOnly(boolean showFailedOnly) { if (this.showFailedOnly != showFailedOnly) { this.showFailedOnly = showFailedOnly; forceRecollectMessages(); } } /** * Returns whether short or long view for file paths should be shown. * * @return filter state */ public boolean getShowFileNameOnly() { return showFileNameOnly; } /** * Sets whether short or long view for file paths should be shown. * * @param showFileNameOnly new filter state */ public void setShowFileNameOnly(boolean showFileNameOnly) { if (this.showFileNameOnly != showFileNameOnly) { this.showFileNameOnly = showFileNameOnly; forceRecollectMessages(); } } /** * Returns whether test messages should be ordered by location. * * @return messages ordering state */ public boolean getOrderingMode() { return orderingMode; } /** * Sets whether test messages should be ordered by location. * * @param orderingMode new messages ordering state */ public void setOrderingMode(boolean orderingMode) { if (this.orderingMode != orderingMode) { this.orderingMode = orderingMode; forceRecollectMessages(); } } /** * Adds the filter message level filters by the message filter action level. * * @param levelFilter message filter action level * @param refresh specifies whether viewer should be refreshed after filter * update (small optimization: avoid many updates on initialization) */ public void addLevelFilter(LevelFilter levelFilter, boolean refresh) { for (ITestMessage.Level level : levelFilter.getLevels()) { acceptedMessageLevels.add(level); } if (refresh) { tableViewer.refresh(); } } /** * Removed the filter message level filters by the message filter action * level. * * @param levelFilter message filter action level */ public void removeLevelFilter(LevelFilter levelFilter) { for (ITestMessage.Level level : levelFilter.getLevels()) { acceptedMessageLevels.remove(level); } tableViewer.refresh(); } }