/*
* 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.project.editor.validation;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.tools.services.IDirtyProviderService;
import org.eclipse.e4.ui.di.UIEventTopic;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.RowExposingTreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerColumnsFactory;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewersConfigurator;
import org.eclipse.jface.window.ToolTip;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.model.WorkbenchContentProvider;
import org.rf.ide.core.project.RobotProjectConfig;
import org.rf.ide.core.project.RobotProjectConfig.ExcludedFolderPath;
import org.robotframework.ide.eclipse.main.plugin.model.LibspecsFolder;
import org.robotframework.ide.eclipse.main.plugin.project.RedProjectConfigEventData;
import org.robotframework.ide.eclipse.main.plugin.project.RobotProjectConfigEvents;
import org.robotframework.ide.eclipse.main.plugin.project.editor.Environments;
import org.robotframework.ide.eclipse.main.plugin.project.editor.RedProjectEditorInput;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.CellsActivationStrategy;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.CellsActivationStrategy.RowTabbingStrategy;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.HeaderFilterMatchesCollection;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.ISectionFormFragment;
import org.robotframework.red.forms.RedFormToolkit;
import org.robotframework.red.swt.SwtThread;
import org.robotframework.red.viewers.Viewers;
import com.google.common.base.Predicate;
/**
* @author Michal Anglart
*/
public class ProjectValidationFormFragment implements ISectionFormFragment {
private static final String CONTEXT_ID = "org.robotframework.ide.eclipse.redxmleditor.validation.context";
@Inject
private IEditorSite site;
@Inject
private RedFormToolkit toolkit;
@Inject
private IDirtyProviderService dirtyProviderService;
@Inject
private RedProjectEditorInput editorInput;
private RowExposingTreeViewer viewer;
private Button excludeFilesBtn;
private Text excludeFilesTxt;
ISelectionProvider getViewer() {
return viewer;
}
@Override
public void initialize(final Composite parent) {
final Section section = createSection(parent);
final Composite internalComposite = toolkit.createComposite(section);
section.setClient(internalComposite);
GridDataFactory.fillDefaults().grab(true, true).applyTo(internalComposite);
GridLayoutFactory.fillDefaults().applyTo(internalComposite);
createViewer(internalComposite);
createColumns();
createContextMenu();
setInput();
installResourceChangeListener();
final Composite excludeFilesComposite = toolkit.createComposite(internalComposite);
GridLayoutFactory.fillDefaults().numColumns(2).margins(0, 10).applyTo(excludeFilesComposite);
createExcludeFilesControls(excludeFilesComposite);
}
private Section createSection(final Composite parent) {
final Section section = toolkit.createSection(parent,
ExpandableComposite.EXPANDED | ExpandableComposite.TITLE_BAR | Section.DESCRIPTION);
section.setText("Excluded project parts");
section.setDescription("Specify parts of the project which shouldn't be validated.");
GridDataFactory.fillDefaults().grab(true, true).applyTo(section);
return section;
}
private void createViewer(final Composite parent) {
viewer = new RowExposingTreeViewer(parent,
SWT.MULTI | SWT.FULL_SELECTION | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
CellsActivationStrategy.addActivationStrategy(viewer, RowTabbingStrategy.MOVE_TO_NEXT);
ColumnViewerToolTipSupport.enableFor(viewer, ToolTip.NO_RECREATE);
GridDataFactory.fillDefaults().grab(true, true).indent(0, 10).applyTo(viewer.getTree());
viewer.setUseHashlookup(true);
viewer.setAutoExpandLevel(2);
viewer.getTree().setEnabled(false);
viewer.setComparator(new ViewerSorter());
viewer.setContentProvider(new WorkbenchContentProvider());
ViewersConfigurator.enableDeselectionPossibility(viewer);
ViewersConfigurator.disableContextMenuOnHeader(viewer);
Viewers.boundViewerWithContext(viewer, site, CONTEXT_ID);
}
private void createColumns() {
ViewerColumnsFactory.newColumn("")
.withWidth(300)
.shouldGrabAllTheSpaceLeft(true)
.withMinWidth(100)
.labelsProvidedBy(new ProjectValidationPathsLabelProvider(editorInput))
.createFor(viewer);
}
private void createExcludeFilesControls(final Composite parent) {
final RobotProjectConfig projectConfiguration = editorInput.getProjectConfiguration();
excludeFilesBtn = toolkit.createButton(parent, "Exclude files by size [KB] greater than:", SWT.CHECK);
excludeFilesBtn.setSelection(projectConfiguration.isValidatedFileSizeCheckingEnabled());
excludeFilesTxt = toolkit.createText(parent, projectConfiguration.getValidatedFileMaxSize());
GridDataFactory.fillDefaults().hint(200, SWT.DEFAULT).applyTo(excludeFilesTxt);
excludeFilesTxt.addVerifyListener(new VerifyListener() {
@Override
public void verifyText(final VerifyEvent e) {
final String string = e.text;
if (string != null) {
final char[] chars = new char[string.length()];
string.getChars(0, chars.length, chars, 0);
for (int i = 0; i < chars.length; i++) {
if (!('0' <= chars[i] && chars[i] <= '9')) {
e.doit = false;
return;
}
}
}
}
});
excludeFilesTxt.addModifyListener(new ModifyListener() {
@Override
public void modifyText(final ModifyEvent e) {
try {
final String fileMaxSizeTxt = excludeFilesTxt.getText();
Long.parseLong(fileMaxSizeTxt);
editorInput.getProjectConfiguration().setValidatedFileMaxSize(fileMaxSizeTxt);
setDirty(true);
} catch (final NumberFormatException e1) {
// nothing to do
}
}
});
excludeFilesBtn.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent e) {
final boolean selection = excludeFilesBtn.getSelection();
excludeFilesTxt.setEnabled(selection);
editorInput.getProjectConfiguration().setIsValidatedFileSizeCheckingEnabled(selection);
setDirty(true);
}
});
}
private void createContextMenu() {
final String menuId = "org.robotframework.ide.eclipse.redxmleditor.validation.contextMenu";
final Tree control = viewer.getTree();
final MenuManager manager = new MenuManager("Red.xml file editor validation context menu", menuId);
manager.setRemoveAllWhenShown(true);
final IMenuListener menuListener = new IMenuListener() {
@Override
public void menuAboutToShow(final IMenuManager menuManager) {
menuManager.add(new Separator("additions"));
}
};
manager.addMenuListener(menuListener);
control.addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(final DisposeEvent e) {
manager.removeMenuListener(menuListener);
}
});
final Menu menu = manager.createContextMenu(control);
control.setMenu(menu);
site.registerContextMenu(menuId, manager, viewer, false);
}
private void setInput() {
if (viewer.getTree() == null || viewer.getTree().isDisposed()
|| !editorInput.getRobotProject().getProject().exists()) {
return;
}
try {
viewer.getTree().setRedraw(false);
final ISelection selection = viewer.getSelection();
final TreeItem topTreeItem = viewer.getTree().getTopItem();
final Object topItem = topTreeItem == null ? null : topTreeItem.getData();
final IProject project = editorInput.getRobotProject().getProject();
final IWorkspaceRoot wsRoot = project.getWorkspace().getRoot();
final ProjectTreeElement wrappedRoot = new ProjectTreeElement(wsRoot, false);
buildTreeFor(wrappedRoot, project);
addMissingEntriesToTree(wrappedRoot, wrappedRoot.getAll());
viewer.setInput(wrappedRoot);
if (topItem != null) {
viewer.setTopItem(topItem);
}
viewer.setSelection(selection);
} catch (final CoreException e) {
throw new IllegalStateException("Unable to read project structure", e);
} finally {
if (viewer.getTree() != null && !viewer.getTree().isDisposed()) {
viewer.getTree().setRedraw(true);
}
}
}
private void buildTreeFor(final ProjectTreeElement parent, final IResource resource) throws CoreException {
if (resource.getName().startsWith(".")
|| LibspecsFolder.get(resource.getProject()).getResource().equals(resource)) {
return;
}
final boolean isExcluded = editorInput.getProjectConfiguration()
.isExcludedFromValidation(resource.getProjectRelativePath().toPortableString());
final ProjectTreeElement wrappedChild = new ProjectTreeElement(resource, isExcluded);
parent.addChild(wrappedChild);
if (!isExcluded && resource instanceof IContainer) {
final IContainer childContainer = (IContainer) resource;
for (final IResource child : childContainer.members()) {
buildTreeFor(wrappedChild, child);
}
}
}
private void addMissingEntriesToTree(final ProjectTreeElement wrappedRoot,
final Collection<ProjectTreeElement> allElements) {
final Set<ProjectTreeElement> excludedShownInTree = getExcludedElementsInTheTree(allElements);
final List<ExcludedFolderPath> allExcluded = editorInput.getProjectConfiguration().getExcludedPath();
final List<ExcludedFolderPath> excludedNotShownInTree = getExcludedNotShownInTheTree(allExcluded,
excludedShownInTree);
for (final ExcludedFolderPath excludedNotShown : excludedNotShownInTree) {
wrappedRoot.createVirtualNodeFor(Path.fromPortableString(excludedNotShown.getPath()));
}
}
private Set<ProjectTreeElement> getExcludedElementsInTheTree(final Collection<ProjectTreeElement> allElements) {
return newHashSet(filter(allElements, new Predicate<ProjectTreeElement>() {
@Override
public boolean apply(final ProjectTreeElement elem) {
return elem.isExcluded();
}
}));
}
private List<ExcludedFolderPath> getExcludedNotShownInTheTree(final List<ExcludedFolderPath> allExcluded,
final Set<ProjectTreeElement> excludedShownInTree) {
final List<ExcludedFolderPath> paths = newArrayList();
for (final ExcludedFolderPath excludedPath : allExcluded) {
boolean isInTree = false;
for (final ProjectTreeElement element : excludedShownInTree) {
if (element.getPath().equals(Path.fromPortableString(excludedPath.getPath()))) {
isInTree = true;
break;
}
}
if (!isInTree) {
paths.add(excludedPath);
}
}
return paths;
}
private void installResourceChangeListener() {
final IResourceChangeListener resourceListener = new IResourceChangeListener() {
@Override
public void resourceChanged(final IResourceChangeEvent event) {
final AtomicBoolean shouldRefresh = new AtomicBoolean(false);
if (event.getType() != IResourceChangeEvent.POST_CHANGE || event.getDelta() == null) {
return;
}
try {
event.getDelta().accept(new IResourceDeltaVisitor() {
@Override
public boolean visit(final IResourceDelta delta) throws CoreException {
if (editorInput.getRobotProject().getProject().equals(delta.getResource().getProject())) {
shouldRefresh.set(true);
return false;
}
return true;
}
});
} catch (final CoreException e) {
// nothing to do
}
if (shouldRefresh.get() && viewer.getTree() != null && !viewer.getTree().isDisposed()) {
SwtThread.syncExec(viewer.getTree().getDisplay(), new Runnable() {
@Override
public void run() {
setInput();
}
});
}
}
};
ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceListener, IResourceChangeEvent.POST_CHANGE);
viewer.getTree().addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(final DisposeEvent e) {
ResourcesPlugin.getWorkspace().removeResourceChangeListener(resourceListener);
}
});
}
@Override
public void setFocus() {
viewer.getTree().setFocus();
}
private void setDirty(final boolean isDirty) {
dirtyProviderService.setDirtyState(isDirty);
}
@Override
public HeaderFilterMatchesCollection collectMatches(final String filter) {
return null;
}
@Inject
@Optional
private void whenEnvironmentLoadingStarted(
@UIEventTopic(RobotProjectConfigEvents.ROBOT_CONFIG_ENV_LOADING_STARTED) final RobotProjectConfig config) {
setInput();
viewer.getTree().setEnabled(false);
excludeFilesBtn.setEnabled(false);
excludeFilesTxt.setEditable(false);
}
@Inject
@Optional
private void whenEnvironmentsWereLoaded(
@UIEventTopic(RobotProjectConfigEvents.ROBOT_CONFIG_ENV_LOADED) final Environments envs) {
viewer.getTree().setEnabled(editorInput.isEditable());
excludeFilesBtn.setEnabled(editorInput.isEditable());
excludeFilesTxt.setEditable(editorInput.isEditable());
}
@Inject
@Optional
private void whenExclusionListChanged(
@UIEventTopic(RobotProjectConfigEvents.ROBOT_CONFIG_VALIDATION_EXCLUSIONS_STRUCTURE_CHANGED) final RedProjectConfigEventData<Collection<IPath>> eventData) {
// some other file model has changed
if (!eventData.getUnderlyingFile().equals(editorInput.getRobotProject().getConfigurationFile())) {
return;
}
setDirty(true);
setInput();
}
private static final class ViewerSorter extends ViewerComparator {
@Override
public int category(final Object element) {
return ((ProjectTreeElement) element).isInternalFolder() ? 0 : 1;
}
@Override
public int compare(final Viewer viewer, final Object e1, final Object e2) {
final int cat1 = category(e1);
final int cat2 = category(e2);
if (cat1 != cat2) {
return cat1 - cat2;
}
final ProjectTreeElement elem1 = (ProjectTreeElement) e1;
final ProjectTreeElement elem2 = (ProjectTreeElement) e2;
final String name1 = elem1.getPath().removeFileExtension().lastSegment();
final String name2 = elem2.getPath().removeFileExtension().lastSegment();
return name1.compareToIgnoreCase(name2);
}
}
}