/* * 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.preferences; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferencePage; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxTableViewer; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.ViewerColumnsFactory; import org.eclipse.jface.viewers.ViewersConfigurator; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.DirectoryDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.ProgressBar; import org.eclipse.swt.widgets.Table; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPreferencePage; import org.eclipse.ui.dialogs.ListSelectionDialog; import org.rf.ide.core.executor.RobotRuntimeEnvironment; import org.rf.ide.core.executor.RobotRuntimeEnvironment.PythonInstallationDirectory; import org.rf.ide.core.executor.SuiteExecutor; import org.robotframework.ide.eclipse.main.plugin.RedPlugin; import org.robotframework.ide.eclipse.main.plugin.RedPreferences; import org.robotframework.ide.eclipse.main.plugin.preferences.InstalledRobotsEnvironmentsLabelProvider.InstalledRobotsNamesLabelProvider; import org.robotframework.ide.eclipse.main.plugin.preferences.InstalledRobotsEnvironmentsLabelProvider.InstalledRobotsPathsLabelProvider; import org.robotframework.ide.eclipse.main.plugin.project.build.RobotProblem; import org.robotframework.red.swt.SwtThread; import org.robotframework.red.viewers.ListInputStructuredContentProvider; import org.robotframework.red.viewers.Selections; import com.google.common.base.Joiner; public class InstalledRobotsPreferencesPage extends PreferencePage implements IWorkbenchPreferencePage { public static final String ID = "org.robotframework.ide.eclipse.main.plugin.preferences.installed"; private List<RobotRuntimeEnvironment> installations; private Composite parent; private CheckboxTableViewer viewer; private ProgressBar progressBar; private Button addButton; private Button removeButton; private Button discoverButton; private boolean dirty = false; public InstalledRobotsPreferencesPage() { super("Installed Robot Frameworks"); } @Override protected IPreferenceStore doGetPreferenceStore() { return RedPlugin.getDefault().getPreferenceStore(); } @Override public void init(final IWorkbench workbench) { // nothing to do } @Override protected Control createContents(final Composite parent) { this.parent = parent; noDefaultAndApplyButton(); GridLayoutFactory.fillDefaults().numColumns(2).applyTo(parent); createDescription(parent); createViewer(parent); addButton = createButton(parent, "Add...", createAddListener()); addButton.setEnabled(false); addButton.setToolTipText("Choose interpreter executable directory"); removeButton = createButton(parent, "Remove", createRemoveListener()); removeButton.setEnabled(false); removeButton.setToolTipText("Remove selected environments"); discoverButton = createButton(parent, "Discover", createDiscoverListener()); discoverButton.setEnabled(false); discoverButton.setToolTipText("Search again for Python interpreters"); createSpacer(parent); progressBar = createProgress(parent); initializeValues(); return parent; } private SelectionListener createDiscoverListener() { return new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent e) { disableControls(); progressBar = createProgress(parent); final QualifiedName key = new QualifiedName(RedPlugin.PLUGIN_ID, "result"); final Job job = new Job("Looking for python installations") { @Override protected IStatus run(final IProgressMonitor monitor) { addOnlyNonExisting(RobotRuntimeEnvironment.whereArePythonInterpreters()); setProperty(key, null); return Status.OK_STATUS; } }; job.addJobChangeListener(new EnvironmentFoundJobListener(key)); job.schedule(); } }; } private boolean addOnlyNonExisting(final Collection<PythonInstallationDirectory> locations) { boolean added = false; for (final PythonInstallationDirectory directory : locations) { boolean contains = false; for (final RobotRuntimeEnvironment environment : installations) { if (environment.getFile().equals(directory)) { contains = true; break; } } if (!contains) { added = true; installations.add(RobotRuntimeEnvironment.create(directory, directory.getInterpreter())); dirty = true; } } return added; } private ProgressBar createProgress(final Composite parent) { final ProgressBar pb = new ProgressBar(parent, SWT.SMOOTH | SWT.INDETERMINATE); pb.setToolTipText("Looking for Python interpreters"); GridDataFactory.fillDefaults().span(2, 1).applyTo(pb); parent.layout(); return pb; } private void destroyProgress(final ProgressBar pb) { final Composite parent = pb.getParent(); pb.dispose(); parent.layout(); } private void createDescription(final Composite parent) { final Label lbl = new Label(parent, SWT.WRAP); lbl.setText("Add or remove Robot frameworks environments (location of Python interpreter with Robot library " + "installed, currently " + Joiner.on(", ").join(SuiteExecutor.allExecutorNames()) + " are supported). The selected environment will be used by project unless it is explicitly " + "overridden in project configuration."); GridDataFactory.fillDefaults().grab(true, false).span(2, 1).hint(600, SWT.DEFAULT).applyTo(lbl); } private void createViewer(final Composite tableParent) { final Table table = new Table(tableParent, SWT.CHECK | SWT.BORDER | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL); viewer = new CheckboxTableViewer(table); ViewersConfigurator.enableDeselectionPossibility(viewer); GridDataFactory.fillDefaults().grab(true, true).span(1, 5).applyTo(viewer.getTable()); viewer.getTable().setLinesVisible(true); viewer.getTable().setHeaderVisible(true); final ISelectionChangedListener selectionListener = new ISelectionChangedListener() { @Override public void selectionChanged(final SelectionChangedEvent event) { final RobotRuntimeEnvironment selectedInstallation = getSelectedInstallation(); removeButton.setEnabled(selectedInstallation != null); } }; final ICheckStateListener checkListener = new ICheckStateListener() { @Override public void checkStateChanged(final CheckStateChangedEvent event) { if (event.getChecked()) { viewer.setCheckedElements(new Object[] { event.getElement() }); viewer.refresh(); } dirty = true; } }; viewer.addSelectionChangedListener(selectionListener); viewer.addCheckStateListener(checkListener); viewer.getTable().addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(final DisposeEvent e) { viewer.removeSelectionChangedListener(selectionListener); viewer.removeCheckStateListener(checkListener); } }); ColumnViewerToolTipSupport.enableFor(viewer); viewer.setContentProvider(new ListInputStructuredContentProvider()); ViewerColumnsFactory.newColumn("Name") .withWidth(300) .labelsProvidedBy(new InstalledRobotsNamesLabelProvider(viewer)) .createFor(viewer); ViewerColumnsFactory.newColumn("Path") .withWidth(200) .labelsProvidedBy(new InstalledRobotsPathsLabelProvider(viewer)) .createFor(viewer); } private Button createButton(final Composite tableParent, final String buttonLabel, final SelectionListener selectionListener) { final Button button = new Button(tableParent, SWT.PUSH); GridDataFactory.fillDefaults().hint(100, SWT.DEFAULT).applyTo(button); button.setText(buttonLabel); button.addSelectionListener(selectionListener); return button; } private void createSpacer(final Composite tableParent) { new Label(tableParent, SWT.NONE); } private void disableControls() { viewer.getTable().setEnabled(false); addButton.setEnabled(false); removeButton.setEnabled(false); discoverButton.setEnabled(false); } private void enableControls() { viewer.getTable().setEnabled(true); addButton.setEnabled(true); removeButton.setEnabled(true); discoverButton.setEnabled(true); } private void initializeValues() { final QualifiedName key = new QualifiedName(RedPlugin.PLUGIN_ID, "result"); final Job job = new Job("Looking for python installations") { @Override protected IStatus run(final IProgressMonitor monitor) { final RedPreferences preferences = RedPlugin.getDefault().getPreferences(); installations = InstalledRobotEnvironments.getAllRobotInstallation(preferences); final RobotRuntimeEnvironment active = InstalledRobotEnvironments .getActiveRobotInstallation(preferences); setProperty(key, active); return Status.OK_STATUS; } }; job.addJobChangeListener(new EnvironmentFoundJobListener(key)); job.schedule(); } private RobotRuntimeEnvironment getSelectedInstallation() { // multiselection is not possible final List<RobotRuntimeEnvironment> elements = Selections.getElements( (IStructuredSelection) viewer.getSelection(), RobotRuntimeEnvironment.class); return elements.isEmpty() ? null : elements.get(0); } private SelectionListener createAddListener() { return new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent event) { final DirectoryDialog dirDialog = new DirectoryDialog(InstalledRobotsPreferencesPage.this.getShell()); dirDialog.setText("Browse for python installation"); dirDialog.setMessage("Select location of python with robot framework installed"); final String path = dirDialog.open(); if (path != null) { final List<PythonInstallationDirectory> possibleExecutors = RobotRuntimeEnvironment .possibleInstallationsFor(new File(path)); boolean changed = false; if (possibleExecutors.size() > 1) { final List<PythonInstallationDirectory> toAdd = askUsersWhatShouldBeAdded(path, possibleExecutors); changed = addOnlyNonExisting(toAdd); } else { changed = true; installations.add(RobotRuntimeEnvironment.create(path)); } if (changed) { dirty = true; viewer.setSelection(StructuredSelection.EMPTY); viewer.refresh(); } } } private List<PythonInstallationDirectory> askUsersWhatShouldBeAdded(final String path, final List<PythonInstallationDirectory> possibleExecutors) { final List<PythonInstallationDirectory> result = new ArrayList<>(); final ListSelectionDialog dialog = new ListSelectionDialog(getShell(), possibleExecutors, new ListInputStructuredContentProvider(), new InterpretersExecutablesLabelProvider(), "Select which of following interpreters detected inside '" + path + "' should be added:"); if (dialog.open() == Window.OK) { for (final Object object : dialog.getResult()) { final PythonInstallationDirectory executor = (PythonInstallationDirectory) object; result.add(executor); } } return result; } }; } private SelectionListener createRemoveListener() { return new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent event) { final RobotRuntimeEnvironment env = getSelectedInstallation(); installations.remove(env); dirty = true; viewer.setSelection(StructuredSelection.EMPTY); viewer.refresh(); } }; } @Override public boolean performOk() { if (dirty) { final Object[] checkedElement = viewer.getCheckedElements(); final RobotRuntimeEnvironment checkedEnv = checkedElement.length == 0 ? null : (RobotRuntimeEnvironment) checkedElement[0]; final List<String> allPathsList = new ArrayList<>(); final List<String> allExecsList = new ArrayList<>(); for (final RobotRuntimeEnvironment installation : installations) { allPathsList.add(installation.getFile().getAbsolutePath()); allExecsList.add(getExecOf(installation)); } final String activePath = checkedEnv == null ? "" : checkedEnv.getFile().getAbsolutePath(); final String activeExec = checkedEnv == null ? "" : getExecOf(checkedEnv); final String allPaths = Joiner.on(';').join(allPathsList); final String allExecs = Joiner.on(";").join(allExecsList); // The execs has to be stored first, because we're listening on ACTIVE_RUNTIMES // and OTHER_RUNTIMES changes and inside we need actaul value of corresponding // execs preference. This may seem a bit weird to have separated ACTIVE_RUNTIME and // ACTIVE_RUNTIME_EXEC pair, but implementing it this way gives us both directions // versions compatibility. getPreferenceStore().putValue(RedPreferences.ACTIVE_RUNTIME_EXEC, activeExec); getPreferenceStore().putValue(RedPreferences.OTHER_RUNTIMES_EXECS, allExecs); getPreferenceStore().putValue(RedPreferences.ACTIVE_RUNTIME, activePath); getPreferenceStore().putValue(RedPreferences.OTHER_RUNTIMES, allPaths); MessageDialog.openInformation(getShell(), "Rebuild required", "The changes you've made requires full workspace rebuild."); rebuildWorkspace(); return true; } else { return true; } } private String getExecOf(final RobotRuntimeEnvironment installation) { return installation.getFile() instanceof PythonInstallationDirectory ? installation.getInterpreter().name() : ""; } private void rebuildWorkspace() { try { new ProgressMonitorDialog(getShell()).run(true, true, new IRunnableWithProgress() { @Override public void run(final IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { for (final IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects(0)) { try { if (project.exists() && project.isOpen()) { project.deleteMarkers(RobotProblem.TYPE_ID, true, IResource.DEPTH_INFINITE); project.build(IncrementalProjectBuilder.FULL_BUILD, null); } } catch (final CoreException e) { MessageDialog.openError(getShell(), "Workspace rebuild", "Problems occurred during workspace build " + e.getMessage()); } } } }); } catch (InvocationTargetException | InterruptedException e) { MessageDialog.openError(getShell(), "Workspace rebuild", "Problems occurred during workspace build " + e.getMessage()); } } private final class EnvironmentFoundJobListener extends JobChangeAdapter { private final QualifiedName key; private EnvironmentFoundJobListener(final QualifiedName key) { this.key = key; } @Override public void done(final IJobChangeEvent event) { SwtThread.asyncExec(new Runnable() { @Override public void run() { final RobotRuntimeEnvironment active = (RobotRuntimeEnvironment) event.getJob().getProperty(key); final Control control = InstalledRobotsPreferencesPage.this.getControl(); if (control == null || control.isDisposed()) { return; } enableControls(); viewer.setInput(installations); if (active != null) { viewer.setChecked(active, true); viewer.refresh(); } viewer.setSelection(StructuredSelection.EMPTY); destroyProgress(progressBar); progressBar = null; } }); } } private static class InterpretersExecutablesLabelProvider extends LabelProvider { @Override public String getText(final Object element) { return ((PythonInstallationDirectory) element).getInterpreter().executableName(); } } }