blob: 5cb94ba4bf4f0669012b86d5b1b743f7bf8f6c6b [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.preferences;
import static com.android.tools.lint.detector.api.TextFormat.RAW;
import static com.android.tools.lint.detector.api.TextFormat.TEXT;
import com.android.annotations.NonNull;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AdtUtils;
import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.IColorProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.TreeNodeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.TreeViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.PreferencesUtil;
import org.eclipse.ui.dialogs.PropertyPage;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** Preference page for configuring Lint preferences */
public class LintPreferencePage extends PropertyPage implements IWorkbenchPreferencePage,
SelectionListener, ControlListener, ModifyListener {
private static final String ID =
"com.android.ide.eclipse.common.preferences.LintPreferencePage"; //$NON-NLS-1$
private static final int ID_COLUMN_WIDTH = 150;
private EclipseLintClient mClient;
private IssueRegistry mRegistry;
private Configuration mConfiguration;
private IProject mProject;
private Map<Issue, Severity> mSeverities = new HashMap<Issue, Severity>();
private Map<Issue, Severity> mInitialSeverities = Collections.<Issue, Severity>emptyMap();
private boolean mIgnoreEvent;
private Tree mTree;
private TreeViewer mTreeViewer;
private Text mDetailsText;
private Button mCheckFileCheckbox;
private Button mCheckExportCheckbox;
private Link mWorkspaceLink;
private TreeColumn mNameColumn;
private TreeColumn mIdColumn;
private Combo mSeverityCombo;
private Button mIncludeAll;
private Button mIgnoreAll;
private Text mSearch;
/**
* Create the preference page.
*/
public LintPreferencePage() {
setPreferenceStore(AdtPlugin.getDefault().getPreferenceStore());
}
@Override
public Control createContents(Composite parent) {
IAdaptable resource = getElement();
if (resource != null) {
mProject = (IProject) resource.getAdapter(IProject.class);
}
Composite container = new Composite(parent, SWT.NULL);
container.setLayout(new GridLayout(2, false));
if (mProject != null) {
Label projectLabel = new Label(container, SWT.CHECK);
projectLabel.setLayoutData(new GridData(SWT.LEFT, SWT.BOTTOM, false, false, 1,
1));
projectLabel.setText("Project-specific configuration:");
mWorkspaceLink = new Link(container, SWT.NONE);
mWorkspaceLink.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
mWorkspaceLink.setText("<a>Configure Workspace Settings...</a>");
mWorkspaceLink.addSelectionListener(this);
} else {
mCheckFileCheckbox = new Button(container, SWT.CHECK);
mCheckFileCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false,
2, 1));
mCheckFileCheckbox.setSelection(true);
mCheckFileCheckbox.setText("When saving files, check for errors");
mCheckExportCheckbox = new Button(container, SWT.CHECK);
mCheckExportCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false,
2, 1));
mCheckExportCheckbox.setSelection(true);
mCheckExportCheckbox.setText("Run full error check when exporting app and abort if fatal errors are found");
Label separator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL);
separator.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1));
Label checkListLabel = new Label(container, SWT.NONE);
checkListLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
checkListLabel.setText("Issues:");
}
mRegistry = EclipseLintClient.getRegistry();
mClient = new EclipseLintClient(mRegistry,
mProject != null ? Collections.singletonList(mProject) : null, null, false);
Project project = null;
if (mProject != null) {
File dir = AdtUtils.getAbsolutePath(mProject).toFile();
project = mClient.getProject(dir, dir);
}
mConfiguration = mClient.getConfigurationFor(project);
mSearch = new Text(container, SWT.SEARCH | SWT.ICON_CANCEL | SWT.ICON_SEARCH);
mSearch.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
mSearch.addSelectionListener(this);
mSearch.addModifyListener(this);
// Grab the Enter key such that pressing return in the search box filters (instead
// of closing the options dialog)
mSearch.setMessage("type filter text (use ~ to filter by severity, e.g. ~ignore)");
mSearch.addTraverseListener(new TraverseListener() {
@Override
public void keyTraversed(TraverseEvent e) {
if (e.keyCode == SWT.CR) {
updateFilter();
e.doit = false;
} else if (e.keyCode == SWT.ARROW_DOWN) {
// Allow moving from the search into the table
if (mTree.getItemCount() > 0) {
TreeItem firstCategory = mTree.getItem(0);
if (firstCategory.getItemCount() > 0) {
TreeItem first = firstCategory.getItem(0);
mTree.setFocus();
mTree.select(first);
}
}
}
}
});
mTreeViewer = new TreeViewer(container, SWT.BORDER | SWT.FULL_SELECTION);
mTree = mTreeViewer.getTree();
GridData gdTable = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1);
gdTable.widthHint = 500;
gdTable.heightHint = 160;
mTree.setLayoutData(gdTable);
mTree.setLinesVisible(true);
mTree.setHeaderVisible(true);
TreeViewerColumn column1 = new TreeViewerColumn(mTreeViewer, SWT.NONE);
mIdColumn = column1.getColumn();
mIdColumn.setWidth(100);
mIdColumn.setText("Id");
TreeViewerColumn column2 = new TreeViewerColumn(mTreeViewer, SWT.FILL);
mNameColumn = column2.getColumn();
mNameColumn.setWidth(100);
mNameColumn.setText("Name");
mTreeViewer.setContentProvider(new ContentProvider());
mTreeViewer.setLabelProvider(new LabelProvider());
mDetailsText = new Text(container, SWT.BORDER | SWT.READ_ONLY | SWT.WRAP |SWT.V_SCROLL
| SWT.MULTI);
GridData gdText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 2);
gdText.heightHint = 80;
mDetailsText.setLayoutData(gdText);
Label severityLabel = new Label(container, SWT.NONE);
severityLabel.setText("Severity:");
mSeverityCombo = new Combo(container, SWT.READ_ONLY);
mSeverityCombo.setItems(new String[] {
"(Default)", "Fatal", "Error", "Warning", "Information", "Ignore"
});
GridData gdSeverityCombo = new GridData(SWT.FILL, SWT.TOP, false, false, 1, 1);
gdSeverityCombo.widthHint = 90;
mSeverityCombo.setLayoutData(gdSeverityCombo);
mSeverityCombo.setText("");
mSeverityCombo.addSelectionListener(this);
List<Issue> issues = mRegistry.getIssues();
for (Issue issue : issues) {
Severity severity = mConfiguration.getSeverity(issue);
mSeverities.put(issue, severity);
}
mInitialSeverities = new HashMap<Issue, Severity>(mSeverities);
mTreeViewer.setInput(mRegistry);
mTree.addSelectionListener(this);
// Add a listener to resize the column to the full width of the table
mTree.addControlListener(this);
loadSettings(false);
mTreeViewer.expandAll();
return container;
}
/**
* Initialize the preference page.
*/
@Override
public void init(IWorkbench workbench) {
// Initialize the preference page
}
@Override
protected void contributeButtons(Composite parent) {
super.contributeButtons(parent);
// Add "Include All" button for quickly enabling all the detectors, including
// those disabled by default
mIncludeAll = new Button(parent, SWT.PUSH);
mIncludeAll.setText("Include All");
mIncludeAll.addSelectionListener(this);
// Add "Ignore All" button for quickly disabling all the detectors
mIgnoreAll = new Button(parent, SWT.PUSH);
mIgnoreAll.setText("Ignore All");
mIgnoreAll.addSelectionListener(this);
// As per the contributeButton javadoc: increase parent's column count for each
// added button
((GridLayout) parent.getLayout()).numColumns += 2;
}
@Override
public void dispose() {
super.dispose();
cancelPendingSearch();
}
@Override
protected void performDefaults() {
super.performDefaults();
mConfiguration.startBulkEditing();
List<Issue> issues = mRegistry.getIssues();
for (Issue issue : issues) {
mConfiguration.setSeverity(issue, null);
}
mConfiguration.finishBulkEditing();
loadSettings(true);
}
@Override
public boolean performOk() {
storeSettings();
return true;
}
private void loadSettings(boolean refresh) {
if (mCheckExportCheckbox != null) {
AdtPrefs prefs = AdtPrefs.getPrefs();
mCheckFileCheckbox.setSelection(prefs.isLintOnSave());
mCheckExportCheckbox.setSelection(prefs.isLintOnExport());
}
mSeverities.clear();
List<Issue> issues = mRegistry.getIssues();
for (Issue issue : issues) {
Severity severity = mConfiguration.getSeverity(issue);
mSeverities.put(issue, severity);
}
if (refresh) {
mTreeViewer.refresh();
}
}
private void storeSettings() {
// Lint on Save, Lint on Export
if (mCheckExportCheckbox != null) {
AdtPrefs prefs = AdtPrefs.getPrefs();
prefs.setLintOnExport(mCheckExportCheckbox.getSelection());
prefs.setLintOnSave(mCheckFileCheckbox.getSelection());
}
if (mConfiguration == null) {
return;
}
mConfiguration.startBulkEditing();
try {
// Severities
for (Map.Entry<Issue, Severity> entry : mSeverities.entrySet()) {
Issue issue = entry.getKey();
Severity severity = entry.getValue();
if (mConfiguration.getSeverity(issue) != severity) {
if ((severity == issue.getDefaultSeverity()) && issue.isEnabledByDefault()) {
severity = null;
}
mConfiguration.setSeverity(issue, severity);
}
}
} finally {
mConfiguration.finishBulkEditing();
}
if (!mInitialSeverities.equals(mSeverities)) {
// Ask user whether we should re-run the rules.
MessageDialog dialog = new MessageDialog(
null, "Lint Settings Have Changed", null,
"The list of enabled checks has changed. Would you like to run lint now " +
"to update the results?",
MessageDialog.QUESTION,
new String[] {
"Yes", "No"
},
0); // yes is the default
int result = dialog.open();
if (result == 0) {
// Run lint on all the open Android projects
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IProject[] projects = workspace.getRoot().getProjects();
List<IProject> androidProjects = new ArrayList<IProject>(projects.length);
for (IProject project : projects) {
if (project.isOpen() && BaseProjectHelper.isAndroidProject(project)) {
androidProjects.add(project);
}
}
EclipseLintRunner.startLint(androidProjects, null, null, false /*fatalOnly*/,
true /*show*/);
}
}
}
private void updateFilter() {
cancelPendingSearch();
if (!mSearch.isDisposed()) {
// Clear selection before refiltering since otherwise it might be showing
// items no longer available in the list.
mTree.setSelection(new TreeItem[0]);
mDetailsText.setText("");
try {
mIgnoreEvent = true;
mSeverityCombo.setText("");
mSeverityCombo.setEnabled(false);
} finally {
mIgnoreEvent = false;
}
mTreeViewer.getContentProvider().inputChanged(mTreeViewer, null, mRegistry);
mTreeViewer.refresh();
mTreeViewer.expandAll();
}
}
private void cancelPendingSearch() {
if (mPendingUpdate != null) {
Shell shell = getShell();
if (!shell.isDisposed()) {
getShell().getDisplay().timerExec(-1, mPendingUpdate);
}
mPendingUpdate = null;
}
}
private Runnable mPendingUpdate;
private void scheduleSearch() {
if (mPendingUpdate == null) {
mPendingUpdate = new Runnable() {
@Override
public void run() {
mPendingUpdate = null;
updateFilter();
}
};
}
getShell().getDisplay().timerExec(250 /*ms*/, mPendingUpdate);
}
// ---- Implements SelectionListener ----
@Override
public void widgetSelected(SelectionEvent e) {
if (mIgnoreEvent) {
return;
}
Object source = e.getSource();
if (source == mTree) {
TreeItem item = (TreeItem) e.item;
Object data = item != null ? item.getData() : null;
if (data instanceof Issue) {
Issue issue = (Issue) data;
String summary = issue.getBriefDescription(TextFormat.TEXT);
String explanation = issue.getExplanation(TextFormat.TEXT);
StringBuilder sb = new StringBuilder(summary.length() + explanation.length() + 20);
sb.append(summary);
sb.append('\n').append('\n');
sb.append(explanation);
mDetailsText.setText(sb.toString());
try {
mIgnoreEvent = true;
Severity severity = getSeverity(issue);
mSeverityCombo.select(severity.ordinal() + 1); // Skip the default option
mSeverityCombo.setEnabled(true);
} finally {
mIgnoreEvent = false;
}
} else {
mDetailsText.setText("");
try {
mIgnoreEvent = true;
mSeverityCombo.setText("");
mSeverityCombo.setEnabled(false);
} finally {
mIgnoreEvent = false;
}
}
} else if (source == mWorkspaceLink) {
int result = PreferencesUtil.createPreferenceDialogOn(getShell(), ID,
new String[] { ID }, null).open();
if (result == Window.OK) {
loadSettings(true);
}
} else if (source == mSeverityCombo) {
int index = mSeverityCombo.getSelectionIndex();
Issue issue = (Issue) mTree.getSelection()[0].getData();
Severity severity;
if (index == -1 || index == 0) {
// "(Default)"
severity = issue.getDefaultSeverity();
} else {
// -1: Skip the "(Default)"
severity = Severity.values()[index - 1];
}
mSeverities.put(issue, severity);
mTreeViewer.refresh();
} else if (source == mIncludeAll) {
List<Issue> issues = mRegistry.getIssues();
for (Issue issue : issues) {
// The default severity is never ignore; for disabled-by-default
// issues the {@link Issue#isEnabledByDefault()} method is false instead
mSeverities.put(issue, issue.getDefaultSeverity());
}
mTreeViewer.refresh();
} else if (source == mIgnoreAll) {
List<Issue> issues = mRegistry.getIssues();
for (Issue issue : issues) {
mSeverities.put(issue, Severity.IGNORE);
}
mTreeViewer.refresh();
} else if (source == mSearch) {
updateFilter();
}
}
private Severity getSeverity(Issue issue) {
Severity severity = mSeverities.get(issue);
if (severity != null) {
return severity;
}
return mConfiguration.getSeverity(issue);
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
Object source = e.getSource();
if (source == mTree) {
widgetSelected(e);
} else if (source == mSearch) {
if (e.detail == SWT.CANCEL) {
// Cancel the search
mSearch.setText("");
}
updateFilter();
}
}
// ---- Implements ModifyListener ----
@Override
public void modifyText(ModifyEvent e) {
if (e.getSource() == mSearch) {
scheduleSearch();
}
}
// ---- Implements ControlListener ----
@Override
public void controlMoved(ControlEvent e) {
}
@Override
public void controlResized(ControlEvent e) {
Rectangle r = mTree.getClientArea();
int availableWidth = r.width;
mIdColumn.setWidth(ID_COLUMN_WIDTH);
availableWidth -= ID_COLUMN_WIDTH;
// Name absorbs everything else
mNameColumn.setWidth(availableWidth);
}
private boolean filterMatches(@NonNull String filter, @NonNull Issue issue) {
return (filter.startsWith("~") //$NON-NLS-1$
&& mConfiguration.getSeverity(issue).getDescription()
.toLowerCase(Locale.US).startsWith(filter.substring(1)))
|| issue.getCategory().getName().toLowerCase(Locale.US).startsWith(filter)
|| issue.getCategory().getFullName().toLowerCase(Locale.US).startsWith(filter)
|| issue.getId().toLowerCase(Locale.US).contains(filter)
|| issue.getBriefDescription(RAW).toLowerCase(Locale.US).contains(filter);
}
private class ContentProvider extends TreeNodeContentProvider {
private Map<Category, List<Issue>> mCategoryToIssues;
private Object[] mElements;
@Override
public Object[] getElements(Object inputElement) {
return mElements;
}
@Override
public boolean hasChildren(Object element) {
return element instanceof Category;
}
@Override
public Object[] getChildren(Object parentElement) {
assert mCategoryToIssues != null;
List<Issue> list = mCategoryToIssues.get(parentElement);
if (list == null) {
return new Object[0];
} else {
return list.toArray();
}
}
@Override
public Object getParent(Object element) {
return null;
}
@Override
public void inputChanged(final Viewer viewer, final Object oldInput,
final Object newInput) {
mCategoryToIssues = null;
String filter = mSearch.isDisposed() ? "" : mSearch.getText().trim();
if (filter.length() == 0) {
filter = null;
} else {
filter = filter.toLowerCase(Locale.US);
}
mCategoryToIssues = new HashMap<Category, List<Issue>>();
List<Issue> issues = mRegistry.getIssues();
for (Issue issue : issues) {
if (filter == null || filterMatches(filter, issue)) {
List<Issue> list = mCategoryToIssues.get(issue.getCategory());
if (list == null) {
list = new ArrayList<Issue>();
mCategoryToIssues.put(issue.getCategory(), list);
}
list.add(issue);
}
}
if (filter == null) {
// Not filtering: show all categories
mElements = mRegistry.getCategories().toArray();
} else {
// Filtering: only include categories that contain matches
if (mCategoryToIssues == null) {
getChildren(null);
}
// Preserve the current category order, so instead of
// just creating a list of the mCategoryToIssues keyset, add them
// in the order they appear in in the registry
List<Category> categories = new ArrayList<Category>(mCategoryToIssues.size());
for (Category category : mRegistry.getCategories()) {
if (mCategoryToIssues.containsKey(category)) {
categories.add(category);
}
}
mElements = categories.toArray();
}
}
}
private class LabelProvider implements ITableLabelProvider, IColorProvider {
@Override
public void addListener(ILabelProviderListener listener) {
}
@Override
public void dispose() {
}
@Override
public boolean isLabelProperty(Object element, String property) {
return true;
}
@Override
public void removeListener(ILabelProviderListener listener) {
}
@Override
public Image getColumnImage(Object element, int columnIndex) {
if (element instanceof Category) {
return null;
}
if (columnIndex == 1) {
Issue issue = (Issue) element;
Severity severity = mSeverities.get(issue);
if (severity == null) {
return null;
}
ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
switch (severity) {
case FATAL:
case ERROR:
return sharedImages.getImage(ISharedImages.IMG_OBJS_ERROR_TSK);
case WARNING:
return sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK);
case INFORMATIONAL:
return sharedImages.getImage(ISharedImages.IMG_OBJS_INFO_TSK);
case IGNORE:
return sharedImages.getImage(ISharedImages.IMG_ELCL_REMOVE_DISABLED);
}
}
return null;
}
@Override
public String getColumnText(Object element, int columnIndex) {
if (element instanceof Category) {
if (columnIndex == 0) {
return ((Category) element).getFullName();
} else {
return null;
}
}
Issue issue = (Issue) element;
switch (columnIndex) {
case 0:
return issue.getId();
case 1:
return issue.getBriefDescription(TEXT);
}
return null;
}
// ---- IColorProvider ----
@Override
public Color getForeground(Object element) {
if (element instanceof Category) {
return mTree.getDisplay().getSystemColor(SWT.COLOR_INFO_FOREGROUND);
}
if (element instanceof Issue) {
Issue issue = (Issue) element;
Severity severity = mSeverities.get(issue);
if (severity == Severity.IGNORE) {
return mTree.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
}
}
return null;
}
@Override
public Color getBackground(Object element) {
if (element instanceof Category) {
return mTree.getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND);
}
return null;
}
}
}