/*
* Copyright 2015 Nokia Solutions and Networks
* Licensed under the Apache License, Version 2.0,
* see license.txt file for details.
*/
package org.robotframework.ide.eclipse.main.plugin.tableeditor;
import static com.google.common.collect.Lists.newArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
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.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerColumnsFactory;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.custom.CaretEvent;
import org.eclipse.swt.custom.CaretListener;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
import org.robotframework.ide.eclipse.main.plugin.RedImages;
import org.robotframework.ide.eclipse.main.plugin.model.RobotCodeHoldingElement;
import org.robotframework.ide.eclipse.main.plugin.model.RobotContainer;
import org.robotframework.ide.eclipse.main.plugin.model.RobotElement;
import org.robotframework.ide.eclipse.main.plugin.model.RobotFileInternalElement;
import org.robotframework.ide.eclipse.main.plugin.model.RobotFileInternalElement.DefinitionPosition;
import org.robotframework.ide.eclipse.main.plugin.model.RobotSetting;
import org.robotframework.ide.eclipse.main.plugin.model.RobotSetting.SettingsGroup;
import org.robotframework.ide.eclipse.main.plugin.model.RobotSuiteFile;
import org.robotframework.ide.eclipse.main.plugin.model.RobotSuiteFileSection;
import org.robotframework.ide.eclipse.main.plugin.navigator.ArtificialGroupingRobotElement;
import org.robotframework.ide.eclipse.main.plugin.navigator.NavigatorLabelProvider;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.source.SuiteSourceEditor;
import org.robotframework.red.actions.ExpandAllAction;
import org.robotframework.red.viewers.Selections;
import org.robotframework.red.viewers.Viewers;
class RobotOutlinePage extends ContentOutlinePage {
public static final String CONTEXT_ID = "org.robotframework.ide.eclipse.tableeditor.outline.context";
private final RobotFormEditor editor;
private final RobotSuiteFile suiteModel;
private RobotOutlineContentProvider contentProvider;
private ISelectionChangedListener outlineSelectionListener;
private ISelectionChangedListener editorSelectionListener;
private CaretListener caretListener;
private final AtomicBoolean shouldUpdateEditorSelection = new AtomicBoolean(true);
private Job modelQueryJob;
public RobotOutlinePage(final RobotFormEditor editor, final RobotSuiteFile suiteModel) {
this.editor = editor;
this.suiteModel = suiteModel;
}
@Override
public void createControl(final Composite parent) {
super.createControl(parent);
contentProvider = new RobotOutlineContentProvider();
getTreeViewer().setContentProvider(contentProvider);
final NavigatorLabelProvider labelProvider = new NavigatorLabelProvider();
ViewerColumnsFactory.newColumn("")
.withWidth(400)
.labelsProvidedBy(labelProvider)
.createFor(getTreeViewer());
getTreeViewer().setInput(new Object[] { suiteModel });
getTreeViewer().expandToLevel(3);
final ISelectionProvider editorSelectionProvider = editor.getSite().getSelectionProvider();
final StyledText editorSourceWidget = editor.getSourceEditor().getViewer().getTextWidget();
editorSelectionListener = createEditorSelectionListener();
editorSelectionProvider.addSelectionChangedListener(editorSelectionListener);
outlineSelectionListener = createOutlineSelectionListener();
getTreeViewer().addSelectionChangedListener(outlineSelectionListener);
caretListener = createCaretListener();
editorSourceWidget.addCaretListener(caretListener);
getSite().getActionBars().getToolBarManager().add(
new LinkWithEditorAction(editorSelectionProvider, editorSourceWidget));
getSite().getActionBars().getToolBarManager().add(new SortOutlineAction(labelProvider));
getSite().getActionBars().getToolBarManager().add(new ExpandAllAction(getTreeViewer()));
final MenuManager menuManager = new MenuManager("Outline popup", "RobotOutlinePage.popup");
final Menu menu = menuManager.createContextMenu(getTreeViewer().getControl());
getTreeViewer().getControl().setMenu(menu);
getSite().registerContextMenu ("RobotOutlinePage.popup", menuManager, getTreeViewer());
Viewers.boundViewerWithContext(getTreeViewer(), getSite(), CONTEXT_ID);
}
private CaretListener createCaretListener() {
return new CaretListener() {
@Override
public void caretMoved(final CaretEvent event) {
if (modelQueryJob != null && modelQueryJob.getState() == Job.SLEEPING) {
modelQueryJob.cancel();
}
modelQueryJob = createModelQueryJob(event.display, event.caretOffset);
modelQueryJob.schedule(300);
}
};
}
private Job createModelQueryJob(final Display display, final int caretOffset) {
final Job job = new Job("Looking for model element") {
@Override
protected IStatus run(final IProgressMonitor monitor) {
final Optional<? extends RobotElement> element = suiteModel.findElement(caretOffset);
if (element.isPresent() && !display.isDisposed()) {
display.asyncExec(new Runnable() {
@Override
public void run() {
shouldUpdateEditorSelection.set(false);
setSelectionInOutline(
element.isPresent() && element.get() == suiteModel
? Optional.<RobotElement> empty() : element);
}
});
}
return Status.OK_STATUS;
}
};
job.setSystem(true);
return job;
}
private ISelectionChangedListener createEditorSelectionListener() {
return new ISelectionChangedListener() {
@Override
public void selectionChanged(final SelectionChangedEvent event) {
if (event.getSelection() instanceof IStructuredSelection) {
final Optional<RobotElement> element = Selections
.getOptionalFirstElement((IStructuredSelection) event.getSelection(), RobotElement.class);
if (element.isPresent()) {
shouldUpdateEditorSelection.set(false);
setSelectionInOutline(element);
}
}
}
};
}
private ISelectionChangedListener createOutlineSelectionListener() {
return new ISelectionChangedListener() {
@Override
public void selectionChanged(final SelectionChangedEvent event) {
if (!shouldUpdateEditorSelection.getAndSet(true)) {
return;
}
final Optional<RobotFileInternalElement> element = Selections.getOptionalFirstElement(
(IStructuredSelection) event.getSelection(), RobotFileInternalElement.class);
if (!element.isPresent()) {
return;
}
final RobotFileInternalElement robotElement = element.get();
if (editor.getActiveEditor() instanceof SuiteSourceEditor) {
final ISelectionProvider selectionProvider = editor.getActiveEditor()
.getSite()
.getSelectionProvider();
final DefinitionPosition position = robotElement.getDefinitionPosition();
selectionProvider.setSelection(new TextSelection(position.getOffset(), position.getLength()));
} else {
final ISectionEditorPart activatedPage = editor.activatePage(getSection(element.get()));
if (activatedPage != null) {
activatedPage.revealElement(element.get());
}
}
}
private RobotSuiteFileSection getSection(final RobotFileInternalElement element) {
RobotElement current = element;
while (current != null && !(current instanceof RobotSuiteFileSection)) {
current = current.getParent();
}
return (RobotSuiteFileSection) current;
}
};
}
private void setSelectionInOutline(final Optional<? extends RobotElement> element) {
if (!element.isPresent()) {
return;
}
if (element.get() instanceof RobotSetting
&& ((RobotSetting) element.get()).getGroup() != SettingsGroup.NO_GROUP) {
getTreeViewer().setSelection(createSelectionForGroupedSetting((RobotSetting) element.get()));
} else {
getTreeViewer().setSelection(new StructuredSelection(new Object[] { element.get() }));
}
}
private ISelection createSelectionForGroupedSetting(final RobotSetting setting) {
final List<Object> pathElements = newArrayList((Object) setting);
final ArtificialGroupingRobotElement groupingElement = contentProvider.getGroupingElement(setting.getGroup());
if (groupingElement != null) {
pathElements.add(groupingElement);
}
RobotElement current = setting.getParent();
while (current != null && !(current instanceof RobotContainer)) {
pathElements.add(current);
current = current.getParent();
}
Collections.reverse(pathElements);
return new TreeSelection(new TreePath(pathElements.toArray()));
}
@Override
public void dispose() {
editor.getSite().getSelectionProvider().removeSelectionChangedListener(editorSelectionListener);
getTreeViewer().removeSelectionChangedListener(outlineSelectionListener);
super.dispose();
}
private class LinkWithEditorAction extends Action {
private final ISelectionProvider editorSelectionProvider;
private final StyledText editorSourceWidget;
public LinkWithEditorAction(final ISelectionProvider editorSelectionProvider,
final StyledText editorSourceWidget) {
super("Link with Editor", IAction.AS_CHECK_BOX);
setImageDescriptor(RedImages.getLinkImage());
this.editorSelectionProvider = editorSelectionProvider;
this.editorSourceWidget = editorSourceWidget;
setChecked(true);
}
@Override
public void run() {
if (isChecked()) {
editorSelectionProvider.addSelectionChangedListener(editorSelectionListener);
editorSourceWidget.addCaretListener(caretListener);
} else {
editorSelectionProvider.removeSelectionChangedListener(editorSelectionListener);
editorSourceWidget.removeCaretListener(caretListener);
}
}
}
private class SortOutlineAction extends Action {
private final NavigatorLabelProvider labelProvider;
public SortOutlineAction(final NavigatorLabelProvider labelProvider) {
super("Sort", IAction.AS_CHECK_BOX);
setImageDescriptor(RedImages.getSortImage());
this.labelProvider = labelProvider;
}
@Override
public void run() {
if (isChecked()) {
getTreeViewer().setComparator(new ViewerComparator() {
@Override
public int compare(final Viewer viewer, final Object e1, final Object e2) {
if (((RobotElement) e1).getParent() instanceof RobotCodeHoldingElement
&& ((RobotElement) e2).getParent() instanceof RobotCodeHoldingElement) {
// we don't want to sort the code inside keywords/test cases
final RobotElement el1 = (RobotElement) e1;
final RobotElement el2 = (RobotElement) e2;
final int index1 = el1.getParent().getChildren().indexOf(el1);
final int index2 = el2.getParent().getChildren().indexOf(el2);
return index1 - index2;
} else {
return labelProvider.getText(e1).compareToIgnoreCase(labelProvider.getText(e2));
}
}
});
} else {
getTreeViewer().setComparator(null);
}
}
}
}