/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.gui.workflow.view.console;
import java.util.Collection;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ILabelDecorator;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MenuDetectEvent;
import org.eclipse.swt.events.MenuDetectListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.ViewPart;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.management.WorkflowHostSetListener;
import de.rcenvironment.core.component.workflow.execution.api.ConsoleModelSnapshot;
import de.rcenvironment.core.component.workflow.execution.api.ConsoleRowFilter;
import de.rcenvironment.core.component.workflow.execution.api.ConsoleRowModelService;
import de.rcenvironment.core.gui.resources.api.FontManager;
import de.rcenvironment.core.gui.resources.api.StandardFonts;
import de.rcenvironment.core.gui.workflow.Activator;
import de.rcenvironment.core.gui.workflow.parts.WorkflowRunNodePart.ComponentStateFigureImpl;
import de.rcenvironment.core.utils.incubator.ServiceRegistry;
import de.rcenvironment.core.utils.incubator.ServiceRegistryPublisherAccess;
/**
* The workflow console view.
*
* @author Enrico Tappert
* @author Doreen Seider
* @author Robert Mischke
*/
public class ConsoleView extends ViewPart {
/** Identifier used to find the this view within the workbench. */
public static final String ID = "de.rcenvironment.gui.WorkflowComponentConsole"; //$NON-NLS-1$
/** The index of "no selection" in a SWT combo box. */
private static final int COMBO_NO_SELECTION = -1;
private static ConsoleView instance;
private static final boolean STDERR_PRESELECTED = true;
private static final boolean STDOUT_PRESELECTED = true;
private static final boolean COMP_LOG_PRESELECTED = true;
private static final int COLUMN_WIDTH_TYPE = 70;
private static final int COLUMN_WIDTH_TIME = 150;
private static final int COLUMN_WIDTH_STD = 500;
private static final int COLUMN_WIDTH_COMPONENT = 100;
private static final int COLUMN_WIDTH_WORKFLOW = 400;
private static final int INITIAL_SELECTION = 0;
private static final int NO_SPACE = 0;
private static final int WORKFLOW_WIDTH = 250;
private static final int COMPONENT_WIDTH = WORKFLOW_WIDTH;
private static final int TEXT_WIDTH = WORKFLOW_WIDTH;
private static final long MODEL_QUERY_MIN_INTERVAL = 500;
private Button checkboxStderr;
private Button checkboxStdout;
private Button checkboxMetaInfo;
private Combo workflowCombo;
private Combo componentCombo;
private TableViewer consoleRowTableViewer;
private ConsoleColumnSorter consoleColumnSorter;
private Text searchTextField;
private int lastKnownSequenceId = ConsoleRowModelService.INITIAL_SEQUENCE_ID;
private final Timer modelQueryTimer = new Timer();
private final ConsoleRowModelService consoleModel;
private final ConsoleRowFilter rowFilter = new ConsoleRowFilter();
private volatile boolean scrollLock = false;
private Button deleteSearchButton;
private Display display;
private ServiceRegistryPublisherAccess serviceRegistryAccess;
private MenuItem copyLineItem;
private MenuItem copyMessageItem;
/**
* A timer task that is used to periodically check the {@link ConsoleRowModelService} for modifications. This approach was chosen over a
* callback/observer structure to realize rate limiting of GUI updates to ensure responsiveness in high CPU load situations.
*
* @author Robert Mischke
*/
private class QueryModelForChangesTask extends TimerTask {
@Override
public void run() {
// Note: this task assumes that it is not invoked concurrently
ConsoleModelSnapshot snapshot = consoleModel.getSnapshotIfModifiedSince(lastKnownSequenceId);
if (snapshot == null) {
return;
}
lastKnownSequenceId = snapshot.getSequenceId();
// apply synchronously for rate limiting
syncApplySnapshot(snapshot);
}
}
/**
* A Runnable that applies a given {@link ConsoleModelSnapshot} to the GUI. Intended to run from the SWT event dispatch thread.
*
* @author Robert Mischke
*/
private class ApplySnapshotRunnable implements Runnable {
private ConsoleModelSnapshot snapshot;
/**
* Constructor.
*/
ApplySnapshotRunnable(ConsoleModelSnapshot snapshot) {
this.snapshot = snapshot;
}
/**
* Apply the given snapshot to the UI.
*/
@Override
public void run() {
if (workflowCombo.isDisposed()) {
// do nothing if the view was disposed in the meantime
return;
}
// TODO check if these combo modifications trigger unnecessary filter/model updates
// update dropdown boxes if needed
if (snapshot.hasWorkflowListChanged()) {
String oldSelection = getWorkflowSelection();
String[] newList = convertToDisplayArray(snapshot.getWorkflowList());
workflowCombo.setItems(newList);
// restore selection to same value, if possible
workflowCombo.select(INITIAL_SELECTION);
for (int i = 1; i < newList.length; i++) {
// note: oldSelection may be null
if (newList[i].equals(oldSelection)) {
workflowCombo.select(i);
break;
}
}
}
if (snapshot.hasComponentListChanged()) {
String oldSelection = getComponentSelection();
String[] newList = convertToDisplayArray(snapshot.getComponentList());
componentCombo.setItems(newList);
// restore selection to same value, if possible
componentCombo.select(INITIAL_SELECTION);
for (int i = 1; i < newList.length; i++) {
// note: oldSelection may be null
if (newList[i].equals(oldSelection)) {
componentCombo.select(i);
break;
}
}
}
if (snapshot.hasFilteredRowListChanged()) {
consoleRowTableViewer.setInput(snapshot.getFilteredRows());
if (!scrollLock) {
consoleRowTableViewer.getTable().setTopIndex(snapshot.getFilteredRows().size());
consoleRowTableViewer.getTable().getVerticalBar().setSelection(
consoleRowTableViewer.getTable().getVerticalBar().getMaximum() + snapshot.getFilteredRows().size());
}
// "false" parameter improves performance for de-facto immutable rows
consoleRowTableViewer.refresh(true);
}
}
private String[] convertToDisplayArray(Collection<String> input) {
String[] result = new String[input.size() + 1];
result[0] = Messages.all;
int i = 1;
for (String wf : input) {
result[i] = wf;
i++;
}
return result;
}
}
public ConsoleView() {
instance = this;
serviceRegistryAccess = ServiceRegistry.createPublisherAccessFor(this);
consoleModel = serviceRegistryAccess.getService(ConsoleRowModelService.class);
// enqueue the polling task at a non-fixed rate for simple
// update rate limiting in high CPU load situations
// TODO move to an initialization callback instead of constructor
modelQueryTimer.schedule(new QueryModelForChangesTask(), MODEL_QUERY_MIN_INTERVAL, MODEL_QUERY_MIN_INTERVAL);
}
public static ConsoleView getInstance() {
return instance;
}
@Override
public void createPartControl(Composite parent) {
parent.setLayout(new GridLayout(1, false));
display = parent.getShell().getDisplay();
// filter = level selection, platform selection, and search text field
Composite filterComposite = new Composite(parent, SWT.NONE);
filterComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
filterComposite.setLayout(new RowLayout());
createLevelArrangement(filterComposite);
createWorkflowListingArrangement(filterComposite);
createComponentListingArrangement(filterComposite);
createSearchArrangement(filterComposite);
// sash = table and text display
Composite sashComposite = new Composite(parent, SWT.NONE);
sashComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
sashComposite.setLayout(new GridLayout(1, false));
createTableArrangement(sashComposite);
// Triggers if the copy functions in context menu are enabled
consoleRowTableViewer.getTable().addMenuDetectListener(new MenuDetectListener() {
@Override
public void menuDetected(MenuDetectEvent event) {
copyLineItem.setEnabled(!consoleRowTableViewer.getSelection().isEmpty());
copyMessageItem.setEnabled(!consoleRowTableViewer.getSelection().isEmpty());
}
});
// set text field listener
searchTextField.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent arg0) {
rowFilter.setSearchTerm(searchTextField.getText());
applyNewRowFilter(true);
if (searchTextField.getText().equals("")) {
deleteSearchButton.setEnabled(false);
} else {
deleteSearchButton.setEnabled(true);
}
}
});
// create common change/selection listener
SelectionListener changeListener = new SelectionListener() {
@Override
public void widgetSelected(SelectionEvent arg0) {
// copy UI settings to filter
rowFilter.setIncludeMetaInfo(checkboxMetaInfo.getSelection());
rowFilter.setIncludeLifecylceEvents(checkboxMetaInfo.getSelection());
rowFilter.setIncludeStdout(checkboxStdout.getSelection());
rowFilter.setIncludeStderr(checkboxStderr.getSelection());
rowFilter.setWorkflow(getWorkflowSelection());
rowFilter.setComponent(getComponentSelection());
// apply & trigger update
applyNewRowFilter(true);
}
@Override
public void widgetDefaultSelected(SelectionEvent arg0) {
widgetSelected(arg0);
}
};
deleteSearchButton.addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(SelectionEvent arg0) {
searchTextField.setText("");
rowFilter.setSearchTerm("");
applyNewRowFilter(true);
if (searchTextField.getText().equals("")) {
deleteSearchButton.setEnabled(false);
} else {
deleteSearchButton.setEnabled(true);
}
}
@Override
public void widgetDefaultSelected(SelectionEvent arg0) {
widgetSelected(arg0);
}
});
checkboxStderr.addSelectionListener(changeListener);
checkboxStdout.addSelectionListener(changeListener);
checkboxMetaInfo.addSelectionListener(changeListener);
workflowCombo.addSelectionListener(changeListener);
componentCombo.addSelectionListener(changeListener);
// add sorting functionality
consoleColumnSorter = new ConsoleColumnSorter();
consoleRowTableViewer.setSorter(consoleColumnSorter);
// add toolbar actions (right top of view)
for (Action action : createToolbarActions()) {
getViewSite().getActionBars().getToolBarManager().add(action);
}
// set the initial filter; this also schedules an immediate update
applyNewRowFilter(true);
registerWorkflowHostSetListener();
addHotkeysToTable(consoleRowTableViewer);
}
/**
* Registers an event listener for network changes as an OSGi service (whiteboard pattern).
*
* @param display
*/
// TODO 5.0: move this into the ConsoleRowModelService
private void registerWorkflowHostSetListener() {
serviceRegistryAccess.registerService(WorkflowHostSetListener.class, new WorkflowHostSetListener() {
@Override
public void onReachableWorkflowHostsChanged(Set<InstanceNodeSessionId> reachableWfHosts,
Set<InstanceNodeSessionId> addedWfHosts,
Set<InstanceNodeSessionId> removedWfHosts) {
consoleModel.updateSubscriptions();
display.asyncExec(new Runnable() {
@Override
public void run() {
if (!consoleRowTableViewer.getTable().isDisposed()) {
consoleRowTableViewer.refresh();
}
}
});
}
});
}
/**
* @param true to trigger an immediate list update
*/
private void applyNewRowFilter(boolean triggerUpdate) {
consoleModel.setRowFilter(rowFilter);
if (triggerUpdate) {
triggerImmediateUpdate();
}
// clear selection
consoleRowTableViewer.setSelection(new StructuredSelection());
}
private void triggerImmediateUpdate() {
// add an update task immediately
modelQueryTimer.schedule(new QueryModelForChangesTask(), 0);
}
@Override
public void dispose() {
// shut down the query timer
modelQueryTimer.cancel();
super.dispose();
if (serviceRegistryAccess != null) {
serviceRegistryAccess.dispose();
}
}
/** @return the currently selected workflow, or null if none selected. */
private String getWorkflowSelection() {
if (workflowCombo.getSelectionIndex() != COMBO_NO_SELECTION && workflowCombo.getSelectionIndex() != INITIAL_SELECTION) {
return workflowCombo.getItem(workflowCombo.getSelectionIndex());
} else {
return null;
}
}
/** @return the currently selected component, or null if none selected. */
private String getComponentSelection() {
if (componentCombo.getSelectionIndex() != COMBO_NO_SELECTION && componentCombo.getSelectionIndex() != INITIAL_SELECTION) {
return componentCombo.getItem(componentCombo.getSelectionIndex());
} else {
return null;
}
}
@Override
public void setFocus() {
consoleRowTableViewer.getControl().setFocus();
}
private void syncApplySnapshot(final ConsoleModelSnapshot snapshot) {
if (consoleRowTableViewer != null && !consoleRowTableViewer.getTable().isDisposed()) {
consoleRowTableViewer.getTable().getDisplay().syncExec(new ApplySnapshotRunnable(snapshot));
}
}
/**
* Creates the composite structure with level selection options.
*
* @param filterComposite Parent composite for the level selection options.
*/
private void createLevelArrangement(Composite filterComposite) {
RowLayout rowLayout = new RowLayout();
rowLayout.spacing = NO_SPACE;
filterComposite.setLayout(rowLayout);
// meta info checkbox
checkboxMetaInfo = new Button(filterComposite, SWT.CHECK);
checkboxMetaInfo.setText(Messages.compLog);
checkboxMetaInfo.setSelection(COMP_LOG_PRESELECTED);
// stdout checkbox
checkboxStdout = new Button(filterComposite, SWT.CHECK);
checkboxStdout.setText(Messages.stdout);
checkboxStdout.setSelection(STDOUT_PRESELECTED);
// stderr checkbox
checkboxStderr = new Button(filterComposite, SWT.CHECK);
checkboxStderr.setText(Messages.stderr);
checkboxStderr.setSelection(STDERR_PRESELECTED);
}
/**
* Creates the composite structure with platform selection options.
*
* @param workflowComposite Parent composite for the platform selection options.
*/
private void createWorkflowListingArrangement(Composite workflowComposite) {
RowLayout rowLayout = new RowLayout();
rowLayout.spacing = NO_SPACE;
workflowComposite.setLayout(rowLayout);
// workflow listing combo box
workflowCombo = new Combo(workflowComposite, SWT.DROP_DOWN | SWT.READ_ONLY);
workflowCombo.select(INITIAL_SELECTION);
workflowCombo.setLayoutData(new RowData(WORKFLOW_WIDTH, SWT.DEFAULT));
}
/**
* Creates the composite structure with platform selection options.
*
* @param componentComposite Parent composite for the platform selection options.
*/
private void createComponentListingArrangement(Composite componentComposite) {
RowLayout rowLayout = new RowLayout();
rowLayout.spacing = NO_SPACE;
componentComposite.setLayout(rowLayout);
// compoenent listing combo box
componentCombo = new Combo(componentComposite, SWT.DROP_DOWN | SWT.READ_ONLY);
componentCombo.select(INITIAL_SELECTION);
componentCombo.setLayoutData(new RowData(COMPONENT_WIDTH, SWT.DEFAULT));
}
/**
* Creates the composite structure with search options.
*
* @param searchComposite Parent composite for the search options.
*/
private void createSearchArrangement(Composite searchComposite) {
RowLayout rowLayout = new RowLayout();
rowLayout.spacing = 7;
searchComposite.setLayout(rowLayout);
// search text field
searchTextField = new Text(searchComposite, SWT.SEARCH);
searchTextField.setMessage(Messages.search);
searchTextField.setSize(TEXT_WIDTH, SWT.DEFAULT);
searchTextField.setLayoutData(new RowData(TEXT_WIDTH, SWT.DEFAULT));
deleteSearchButton = new Button(searchComposite, SWT.BUTTON1);
deleteSearchButton.setText(Messages.resetSearch);
if (searchTextField.getText().equals("")) {
deleteSearchButton.setEnabled(false);
}
}
/**
* Creates the composite structure with log display and selection options.
*
* @param platformComposite Parent composite for log display and selection options.
*/
private void createTableArrangement(Composite tableComposite) {
tableComposite.setLayout(new GridLayout());
// create table viewer
consoleRowTableViewer = new TableViewer(tableComposite, SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.MULTI | SWT.VIRTUAL);
consoleRowTableViewer.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
// create table header and column styles
String[] titles = new String[] {
Messages.type,
Messages.timestamp,
Messages.message,
Messages.component,
Messages.workflow
};
int[] bounds = { COLUMN_WIDTH_TYPE, COLUMN_WIDTH_TIME, COLUMN_WIDTH_STD, COLUMN_WIDTH_COMPONENT, COLUMN_WIDTH_WORKFLOW };
// for all columns
for (int i = 0; i < bounds.length; i++) {
final int index = i;
final TableViewerColumn viewerColumn = new TableViewerColumn(consoleRowTableViewer, SWT.NONE);
final TableColumn column = viewerColumn.getColumn();
// set column properties
column.setText(titles[i]);
column.setWidth(bounds[i]);
column.setResizable(true);
column.setMoveable(true);
column.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
consoleColumnSorter.setColumn(index);
int direction = consoleRowTableViewer.getTable().getSortDirection();
if (consoleRowTableViewer.getTable().getSortColumn() == column) {
if (direction == SWT.UP) {
direction = SWT.DOWN;
} else {
direction = SWT.UP;
}
} else {
direction = SWT.UP;
}
consoleRowTableViewer.getTable().setSortDirection(direction);
consoleRowTableViewer.getTable().setSortColumn(column);
consoleRowTableViewer.refresh();
}
});
}
// set table content
consoleRowTableViewer.setContentProvider(new ConsoleContentProvider());
ILabelDecorator decorator = PlatformUI.getWorkbench().getDecoratorManager().getLabelDecorator();
consoleRowTableViewer.setLabelProvider(new DecoratedConsoleLabelProvider(new ConsoleLabelProvider(), decorator));
// set table layout data
final Table table = consoleRowTableViewer.getTable();
table.setHeaderVisible(true);
table.setLinesVisible(false);
table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
// set font & color
table.setFont(FontManager.getInstance().getFont(StandardFonts.CONSOLE_TEXT_FONT));
table.setForeground(tableComposite.getDisplay().getSystemColor(SWT.COLOR_BLACK));
// create copy context menu
Menu contextMenu = new Menu(tableComposite);
copyLineItem = new MenuItem(contextMenu, SWT.PUSH);
copyMessageItem = new MenuItem(contextMenu, SWT.PUSH);
copyMessageItem.setText(Messages.copyMessage + "\tCtrl+Alt+C");
copyMessageItem.setImage(Activator.getInstance().getImageRegistry().get(Activator.IMAGE_COPY));
copyMessageItem.addSelectionListener(new CopyToClipboardListener(consoleRowTableViewer));
copyLineItem.setText(Messages.copyLine + "\tCtrl+C");
copyLineItem.setImage(Activator.getInstance().getImageRegistry().get(Activator.IMAGE_COPY));
copyLineItem.addSelectionListener(new CopyToClipboardListener(consoleRowTableViewer));
consoleRowTableViewer.getControl().setMenu(contextMenu);
}
private Action[] createToolbarActions() {
Action scrollLockAction = new Action(Messages.scrollLock, SWT.TOGGLE) {
@Override
public void run() {
scrollLock = !scrollLock;
}
};
scrollLockAction.setImageDescriptor(ImageDescriptor.createFromURL(
ComponentStateFigureImpl.class.getResource("/resources/icons/scrollLock.gif")));
Action deleteAction = new Action(Messages.clear, ImageDescriptor.createFromImage(PlatformUI.getWorkbench()
.getSharedImages().getImage(ISharedImages.IMG_TOOL_DELETE))) {
@Override
public void run() {
// for now, "clear" means "wipe the model"
consoleModel.clearAll();
}
};
return new Action[] { scrollLockAction, deleteAction };
}
private void addHotkeysToTable(final TableViewer table) {
table.getTable().addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.stateMask == SWT.CTRL && e.keyCode == 'a') {
table.getTable().selectAll();
} else if ((e.stateMask == SWT.CTRL && e.keyCode == 'c') && copyLineItem.isEnabled()) {
CopyToClipboardHelper helper = new CopyToClipboardHelper(table);
helper.copyToClipboard(CopyToClipboardHelper.COPY_LINE);
} else if ((e.stateMask == (SWT.CTRL + SWT.ALT) && (e.keyCode == 'c')) && copyLineItem.isEnabled()) {
CopyToClipboardHelper helper = new CopyToClipboardHelper(table);
helper.copyToClipboard(CopyToClipboardHelper.COPY_MESSAGE);
}
}
});
}
}