Home | History | Annotate | Download | only in lint
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.lint;
     17 
     18 import com.android.ide.eclipse.adt.AdtConstants;
     19 import com.android.ide.eclipse.adt.AdtPlugin;
     20 import com.android.ide.eclipse.adt.AdtUtils;
     21 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     22 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
     23 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutActionBar;
     24 import com.android.tools.lint.client.api.Configuration;
     25 import com.android.tools.lint.client.api.IssueRegistry;
     26 import com.android.tools.lint.client.api.LintClient;
     27 import com.android.tools.lint.detector.api.Issue;
     28 import com.android.tools.lint.detector.api.Severity;
     29 import com.google.common.collect.ArrayListMultimap;
     30 import com.google.common.collect.Multimap;
     31 
     32 import org.eclipse.core.resources.IMarker;
     33 import org.eclipse.core.resources.IMarkerDelta;
     34 import org.eclipse.core.resources.IProject;
     35 import org.eclipse.core.resources.IResource;
     36 import org.eclipse.core.resources.IResourceChangeEvent;
     37 import org.eclipse.core.resources.IResourceChangeListener;
     38 import org.eclipse.core.resources.ResourcesPlugin;
     39 import org.eclipse.core.runtime.IProgressMonitor;
     40 import org.eclipse.core.runtime.IStatus;
     41 import org.eclipse.core.runtime.NullProgressMonitor;
     42 import org.eclipse.core.runtime.Status;
     43 import org.eclipse.jface.operation.IRunnableWithProgress;
     44 import org.eclipse.jface.viewers.ColumnPixelData;
     45 import org.eclipse.jface.viewers.ColumnWeightData;
     46 import org.eclipse.jface.viewers.StructuredSelection;
     47 import org.eclipse.jface.viewers.StyledCellLabelProvider;
     48 import org.eclipse.jface.viewers.StyledString;
     49 import org.eclipse.jface.viewers.TableLayout;
     50 import org.eclipse.jface.viewers.TreeNodeContentProvider;
     51 import org.eclipse.jface.viewers.TreeViewer;
     52 import org.eclipse.jface.viewers.TreeViewerColumn;
     53 import org.eclipse.jface.viewers.Viewer;
     54 import org.eclipse.jface.viewers.ViewerCell;
     55 import org.eclipse.jface.viewers.ViewerComparator;
     56 import org.eclipse.jface.window.Window;
     57 import org.eclipse.swt.SWT;
     58 import org.eclipse.swt.custom.BusyIndicator;
     59 import org.eclipse.swt.events.ControlEvent;
     60 import org.eclipse.swt.events.ControlListener;
     61 import org.eclipse.swt.events.PaintEvent;
     62 import org.eclipse.swt.events.PaintListener;
     63 import org.eclipse.swt.events.SelectionAdapter;
     64 import org.eclipse.swt.events.SelectionEvent;
     65 import org.eclipse.swt.events.SelectionListener;
     66 import org.eclipse.swt.events.TreeEvent;
     67 import org.eclipse.swt.events.TreeListener;
     68 import org.eclipse.swt.graphics.Rectangle;
     69 import org.eclipse.swt.layout.GridData;
     70 import org.eclipse.swt.layout.GridLayout;
     71 import org.eclipse.swt.widgets.Composite;
     72 import org.eclipse.swt.widgets.Event;
     73 import org.eclipse.swt.widgets.Tree;
     74 import org.eclipse.swt.widgets.TreeColumn;
     75 import org.eclipse.swt.widgets.TreeItem;
     76 import org.eclipse.ui.IMemento;
     77 import org.eclipse.ui.IWorkbenchPartSite;
     78 import org.eclipse.ui.PlatformUI;
     79 import org.eclipse.ui.progress.IWorkbenchSiteProgressService;
     80 import org.eclipse.ui.progress.WorkbenchJob;
     81 
     82 import java.lang.reflect.InvocationTargetException;
     83 import java.util.ArrayList;
     84 import java.util.Arrays;
     85 import java.util.Collection;
     86 import java.util.HashMap;
     87 import java.util.HashSet;
     88 import java.util.List;
     89 import java.util.Map;
     90 import java.util.Set;
     91 
     92 /**
     93  * A tree-table widget which shows a list of lint warnings for an underlying
     94  * {@link IResource} such as a file, a project, or a list of projects.
     95  */
     96 class LintList extends Composite implements IResourceChangeListener, ControlListener {
     97     private static final Object UPDATE_MARKERS_FAMILY = new Object();
     98 
     99     // For persistence:
    100     private static final String KEY_WIDTHS = "lintColWidth"; //$NON-NLS-1$
    101     private static final String KEY_VISIBLE = "lintColVisible"; //$NON-NLS-1$
    102     // Mapping SWT TreeColumns to LintColumns
    103     private static final String KEY_COLUMN = "lintColumn"; //$NON-NLS-1$
    104 
    105     private final IWorkbenchPartSite mSite;
    106     private final TreeViewer mTreeViewer;
    107     private final Tree mTree;
    108     private Set<String> mExpandedIds;
    109     private ContentProvider mContentProvider;
    110     private String mSelectedId;
    111     private List<? extends IResource> mResources;
    112     private Configuration mConfiguration;
    113     private final boolean mSingleFile;
    114     private int mErrorCount;
    115     private int mWarningCount;
    116     private final UpdateMarkersJob mUpdateMarkersJob = new UpdateMarkersJob();
    117     private final IssueRegistry mRegistry;
    118     private final IMemento mMemento;
    119     private final LintColumn mMessageColumn = new LintColumn.MessageColumn(this);
    120     private final LintColumn mLineColumn = new LintColumn.LineColumn(this);
    121     private final LintColumn[] mColumns = new LintColumn[] {
    122             mMessageColumn,
    123             new LintColumn.PriorityColumn(this),
    124             new LintColumn.CategoryColumn(this),
    125             new LintColumn.LocationColumn(this),
    126             new LintColumn.FileColumn(this),
    127             new LintColumn.PathColumn(this),
    128             mLineColumn
    129     };
    130     private LintColumn[] mVisibleColumns;
    131 
    132     LintList(IWorkbenchPartSite site, Composite parent, IMemento memento, boolean singleFile) {
    133         super(parent, SWT.NONE);
    134         mSingleFile = singleFile;
    135         mMemento = memento;
    136         mSite = site;
    137         mRegistry = EclipseLintClient.getRegistry();
    138 
    139         GridLayout gridLayout = new GridLayout(1, false);
    140         gridLayout.marginWidth = 0;
    141         gridLayout.marginHeight = 0;
    142         setLayout(gridLayout);
    143 
    144         mTreeViewer = new TreeViewer(this, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI);
    145         mTree = mTreeViewer.getTree();
    146         mTree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
    147 
    148         createColumns();
    149         mTreeViewer.setComparator(new TableComparator());
    150         setSortIndicators();
    151 
    152         mContentProvider = new ContentProvider();
    153         mTreeViewer.setContentProvider(mContentProvider);
    154 
    155         mTree.setLinesVisible(true);
    156         mTree.setHeaderVisible(true);
    157         mTree.addControlListener(this);
    158 
    159         ResourcesPlugin.getWorkspace().addResourceChangeListener(
    160                 this,
    161                 IResourceChangeEvent.POST_CHANGE
    162                         | IResourceChangeEvent.PRE_BUILD
    163                         | IResourceChangeEvent.POST_BUILD);
    164 
    165         // Workaround for https://bugs.eclipse.org/341865
    166         mTree.addPaintListener(new PaintListener() {
    167             @Override
    168             public void paintControl(PaintEvent e) {
    169                 mTreePainted = true;
    170                 mTreeViewer.getTree().removePaintListener(this);
    171             }
    172         });
    173 
    174         // Remember the most recently selected id category such that we can
    175         // attempt to reselect it after a refresh
    176         mTree.addSelectionListener(new SelectionAdapter() {
    177             @Override
    178             public void widgetSelected(SelectionEvent e) {
    179                 List<IMarker> markers = getSelectedMarkers();
    180                 if (markers.size() > 0) {
    181                     mSelectedId = EclipseLintClient.getId(markers.get(0));
    182                 }
    183             }
    184         });
    185         mTree.addTreeListener(new TreeListener() {
    186             @Override
    187             public void treeExpanded(TreeEvent e) {
    188                 Object data = e.item.getData();
    189                 if (data instanceof IMarker) {
    190                     String id = EclipseLintClient.getId((IMarker) data);
    191                     if (id != null) {
    192                         if (mExpandedIds == null) {
    193                             mExpandedIds = new HashSet<String>();
    194                         }
    195                         mExpandedIds.add(id);
    196                     }
    197                 }
    198             }
    199 
    200             @Override
    201             public void treeCollapsed(TreeEvent e) {
    202                 if (mExpandedIds != null) {
    203                     Object data = e.item.getData();
    204                     if (data instanceof IMarker) {
    205                         String id = EclipseLintClient.getId((IMarker) data);
    206                         if (id != null) {
    207                             mExpandedIds.remove(id);
    208                         }
    209                     }
    210                 }
    211             }
    212         });
    213     }
    214 
    215     private boolean mTreePainted;
    216 
    217     private void updateColumnWidths() {
    218         Rectangle r = mTree.getClientArea();
    219         int availableWidth = r.width;
    220         // Add all available size to the first column
    221         for (int i = 1; i < mTree.getColumnCount(); i++) {
    222             TreeColumn column = mTree.getColumn(i);
    223             availableWidth -= column.getWidth();
    224         }
    225         if (availableWidth > 100) {
    226             mTree.getColumn(0).setWidth(availableWidth);
    227         }
    228     }
    229 
    230     public void setResources(List<? extends IResource> resources) {
    231         mResources = resources;
    232 
    233         mConfiguration = null;
    234         for (IResource resource : mResources) {
    235             IProject project = resource.getProject();
    236             if (project != null) {
    237                 // For logging only
    238                 LintClient client = new EclipseLintClient(null, null, null, false);
    239                 mConfiguration = ProjectLintConfiguration.get(client, project, false);
    240                 break;
    241             }
    242         }
    243         if (mConfiguration == null) {
    244             mConfiguration = GlobalLintConfiguration.get();
    245         }
    246 
    247         List<IMarker> markerList = getMarkers();
    248         mTreeViewer.setInput(markerList);
    249         if (mSingleFile) {
    250             expandAll();
    251         }
    252 
    253         // Selecting the first item isn't a good idea since it may not be the first
    254         // item shown in the table (since it does its own sorting), and furthermore we
    255         // may not have all the data yet; this is called when scanning begins, not when
    256         // it's done:
    257         //if (mTree.getItemCount() > 0) {
    258         //    mTree.select(mTree.getItem(0));
    259         //}
    260 
    261         updateColumnWidths(); // in case mSingleFile changed
    262     }
    263 
    264     /** Select the first item */
    265     public void selectFirst() {
    266         if (mTree.getItemCount() > 0) {
    267             mTree.select(mTree.getItem(0));
    268         }
    269     }
    270 
    271     private List<IMarker> getMarkers() {
    272         mErrorCount = mWarningCount = 0;
    273         List<IMarker> markerList = new ArrayList<IMarker>();
    274         if (mResources != null) {
    275             for (IResource resource : mResources) {
    276                 IMarker[] markers = EclipseLintClient.getMarkers(resource);
    277                 for (IMarker marker : markers) {
    278                     markerList.add(marker);
    279                     int severity = marker.getAttribute(IMarker.SEVERITY, 0);
    280                     if (severity == IMarker.SEVERITY_ERROR) {
    281                         mErrorCount++;
    282                     } else if (severity == IMarker.SEVERITY_WARNING) {
    283                         mWarningCount++;
    284                     }
    285                 }
    286             }
    287 
    288             // No need to sort the marker list here; it will be sorted by the tree table model
    289         }
    290         return markerList;
    291     }
    292 
    293     public int getErrorCount() {
    294         return mErrorCount;
    295     }
    296 
    297     public int getWarningCount() {
    298         return mWarningCount;
    299     }
    300 
    301     @Override
    302     protected void checkSubclass() {
    303         // Disable the check that prevents subclassing of SWT components
    304     }
    305 
    306     public void addSelectionListener(SelectionListener listener) {
    307         mTree.addSelectionListener(listener);
    308     }
    309 
    310     public void refresh() {
    311         mTreeViewer.refresh();
    312     }
    313 
    314     public List<IMarker> getSelectedMarkers() {
    315         TreeItem[] selection = mTree.getSelection();
    316         List<IMarker> markers = new ArrayList<IMarker>(selection.length);
    317         for (TreeItem item : selection) {
    318             Object data = item.getData();
    319             if (data instanceof IMarker) {
    320                 markers.add((IMarker) data);
    321             }
    322         }
    323 
    324         return markers;
    325     }
    326 
    327     @Override
    328     public void dispose() {
    329         cancelJobs();
    330         ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    331         super.dispose();
    332     }
    333 
    334     private class ContentProvider extends TreeNodeContentProvider {
    335         private Map<Object, Object[]> mChildren;
    336         private Map<IMarker, Integer> mTypeCount;
    337         private IMarker[] mTopLevels;
    338 
    339         @Override
    340         public Object[] getElements(Object inputElement) {
    341             if (inputElement == null) {
    342                 mTypeCount = null;
    343                 return new IMarker[0];
    344             }
    345 
    346             @SuppressWarnings("unchecked")
    347             List<IMarker> list = (List<IMarker>) inputElement;
    348 
    349             // Partition the children such that at the top level we have one
    350             // marker of each type, and below we have all the duplicates of
    351             // each one of those errors. And for errors with multiple locations,
    352             // there is a third level.
    353             Multimap<String, IMarker> types = ArrayListMultimap.<String, IMarker>create(100, 20);
    354             for (IMarker marker : list) {
    355                 String id = EclipseLintClient.getId(marker);
    356                 types.put(id, marker);
    357             }
    358 
    359             Set<String> ids = types.keySet();
    360 
    361             mChildren = new HashMap<Object, Object[]>(ids.size());
    362             mTypeCount = new HashMap<IMarker, Integer>(ids.size());
    363 
    364             List<IMarker> topLevel = new ArrayList<IMarker>(ids.size());
    365             for (String id : ids) {
    366                 Collection<IMarker> markers = types.get(id);
    367                 int childCount = markers.size();
    368 
    369                 // Must sort the list items in order to have a stable first item
    370                 // (otherwise preserving expanded paths etc won't work)
    371                 TableComparator sorter = getTableSorter();
    372                 IMarker[] array = markers.toArray(new IMarker[markers.size()]);
    373                 sorter.sort(mTreeViewer, array);
    374 
    375                 IMarker topMarker = array[0];
    376                 mTypeCount.put(topMarker, childCount);
    377                 topLevel.add(topMarker);
    378 
    379                 IMarker[] children = Arrays.copyOfRange(array, 1, array.length);
    380                 mChildren.put(topMarker, children);
    381             }
    382 
    383             mTopLevels = topLevel.toArray(new IMarker[topLevel.size()]);
    384             return mTopLevels;
    385         }
    386 
    387         @Override
    388         public boolean hasChildren(Object element) {
    389             Object[] children = mChildren != null ? mChildren.get(element) : null;
    390             return children != null && children.length > 0;
    391         }
    392 
    393         @Override
    394         public Object[] getChildren(Object parentElement) {
    395             Object[] children = mChildren.get(parentElement);
    396             if (children != null) {
    397                 return children;
    398             }
    399 
    400             return new Object[0];
    401         }
    402 
    403         @Override
    404         public Object getParent(Object element) {
    405             return null;
    406         }
    407 
    408         public int getCount(IMarker marker) {
    409             if (mTypeCount != null) {
    410                 Integer count = mTypeCount.get(marker);
    411                 if (count != null) {
    412                     return count.intValue();
    413                 }
    414             }
    415 
    416             return -1;
    417         }
    418 
    419         IMarker[] getTopMarkers() {
    420             return mTopLevels;
    421         }
    422     }
    423 
    424     private class LintColumnLabelProvider extends StyledCellLabelProvider {
    425         private LintColumn mColumn;
    426 
    427         LintColumnLabelProvider(LintColumn column) {
    428             mColumn = column;
    429         }
    430 
    431         @Override
    432         public void update(ViewerCell cell) {
    433             Object element = cell.getElement();
    434             cell.setImage(mColumn.getImage((IMarker) element));
    435             StyledString styledString = mColumn.getStyledValue((IMarker) element);
    436             if (styledString == null) {
    437                 cell.setText(mColumn.getValue((IMarker) element));
    438                 cell.setStyleRanges(null);
    439             } else {
    440                 cell.setText(styledString.toString());
    441                 cell.setStyleRanges(styledString.getStyleRanges());
    442             }
    443             super.update(cell);
    444         }
    445     }
    446 
    447     TreeViewer getTreeViewer() {
    448         return mTreeViewer;
    449     }
    450 
    451     Tree getTree() {
    452         return mTree;
    453     }
    454 
    455     // ---- Implements IResourceChangeListener ----
    456 
    457     @Override
    458     public void resourceChanged(IResourceChangeEvent event) {
    459         if (mResources == null) {
    460             return;
    461         }
    462         IMarkerDelta[] deltas = event.findMarkerDeltas(AdtConstants.MARKER_LINT, true);
    463         if (deltas.length > 0) {
    464             // Update immediately for POST_BUILD events, otherwise do an unconditional
    465             // update after 30 seconds. This matches the logic in Eclipse's ProblemView
    466             // (see the MarkerView class).
    467             if (event.getType() == IResourceChangeEvent.POST_BUILD) {
    468                 cancelJobs();
    469                 getProgressService().schedule(mUpdateMarkersJob, 100);
    470             } else {
    471                 IWorkbenchSiteProgressService progressService = getProgressService();
    472                 if (progressService == null) {
    473                     mUpdateMarkersJob.schedule(30000);
    474                 } else {
    475                     getProgressService().schedule(mUpdateMarkersJob, 30000);
    476                 }
    477             }
    478         }
    479     }
    480 
    481     // ---- Implements ControlListener ----
    482 
    483     @Override
    484     public void controlMoved(ControlEvent e) {
    485     }
    486 
    487     @Override
    488     public void controlResized(ControlEvent e) {
    489         updateColumnWidths();
    490     }
    491 
    492     // ---- Updating Markers ----
    493 
    494     private void cancelJobs() {
    495         mUpdateMarkersJob.cancel();
    496     }
    497 
    498     protected IWorkbenchSiteProgressService getProgressService() {
    499         if (mSite != null) {
    500             Object siteService = mSite.getAdapter(IWorkbenchSiteProgressService.class);
    501             if (siteService != null) {
    502                 return (IWorkbenchSiteProgressService) siteService;
    503             }
    504         }
    505         return null;
    506     }
    507 
    508     private class UpdateMarkersJob extends WorkbenchJob {
    509         UpdateMarkersJob() {
    510             super("Updating Lint Markers");
    511             setSystem(true);
    512         }
    513 
    514         @Override
    515         public IStatus runInUIThread(IProgressMonitor monitor) {
    516             if (mTree.isDisposed()) {
    517                 return Status.CANCEL_STATUS;
    518             }
    519 
    520             mTreeViewer.setInput(null);
    521             List<IMarker> markerList = getMarkers();
    522             if (markerList.size() == 0) {
    523                 LayoutEditorDelegate delegate =
    524                     LayoutEditorDelegate.fromEditor(AdtUtils.getActiveEditor());
    525                 if (delegate != null) {
    526                     GraphicalEditorPart g = delegate.getGraphicalEditor();
    527                     assert g != null;
    528                     LayoutActionBar bar = g == null ? null : g.getLayoutActionBar();
    529                     assert bar != null;
    530                     if (bar != null) {
    531                         bar.updateErrorIndicator();
    532                     }
    533                 }
    534             }
    535             // Trigger selection update
    536             Event updateEvent = new Event();
    537             updateEvent.widget = mTree;
    538             mTree.notifyListeners(SWT.Selection, updateEvent);
    539             mTreeViewer.setInput(markerList);
    540             mTreeViewer.refresh();
    541 
    542             if (mExpandedIds != null) {
    543                 List<IMarker> expanded = new ArrayList<IMarker>(mExpandedIds.size());
    544                 IMarker[] topMarkers = mContentProvider.getTopMarkers();
    545                 if (topMarkers != null) {
    546                     for (IMarker marker : topMarkers) {
    547                         String id = EclipseLintClient.getId(marker);
    548                         if (id != null && mExpandedIds.contains(id)) {
    549                             expanded.add(marker);
    550                         }
    551                     }
    552                 }
    553                 if (!expanded.isEmpty()) {
    554                     mTreeViewer.setExpandedElements(expanded.toArray());
    555                 }
    556             }
    557 
    558             if (mSelectedId != null) {
    559                 IMarker[] topMarkers = mContentProvider.getTopMarkers();
    560                 for (IMarker marker : topMarkers) {
    561                     if (mSelectedId.equals(EclipseLintClient.getId(marker))) {
    562                         mTreeViewer.setSelection(new StructuredSelection(marker), true /*reveal*/);
    563                         break;
    564                     }
    565                 }
    566             }
    567 
    568             return Status.OK_STATUS;
    569         }
    570 
    571         @Override
    572         public boolean shouldRun() {
    573             // Do not run if the change came in before there is a viewer
    574             return PlatformUI.isWorkbenchRunning();
    575         }
    576 
    577         @Override
    578         public boolean belongsTo(Object family) {
    579             return UPDATE_MARKERS_FAMILY == family;
    580         }
    581     }
    582 
    583     /**
    584      * Returns the list of resources being shown in the list
    585      *
    586      * @return the list of resources being shown in this composite
    587      */
    588     public List<? extends IResource> getResources() {
    589         return mResources;
    590     }
    591 
    592     /** Expands all nodes */
    593     public void expandAll() {
    594         mTreeViewer.expandAll();
    595 
    596         if (mExpandedIds == null) {
    597             mExpandedIds = new HashSet<String>();
    598         }
    599         IMarker[] topMarkers = mContentProvider.getTopMarkers();
    600         if (topMarkers != null) {
    601             for (IMarker marker : topMarkers) {
    602                 String id = EclipseLintClient.getId(marker);
    603                 if (id != null) {
    604                     mExpandedIds.add(id);
    605                 }
    606             }
    607         }
    608     }
    609 
    610     /** Collapses all nodes */
    611     public void collapseAll() {
    612         mTreeViewer.collapseAll();
    613         mExpandedIds = null;
    614     }
    615 
    616     // ---- Column Persistence ----
    617 
    618     public void saveState(IMemento memento) {
    619         if (mSingleFile) {
    620             // Don't use persistence for single-file lists: this is a special mode of the
    621             // window where we show a hardcoded set of columns for a single file, deliberately
    622             // omitting the location column etc
    623             return;
    624         }
    625 
    626         IMemento columnEntry = memento.createChild(KEY_WIDTHS);
    627         LintColumn[] columns = new LintColumn[mTree.getColumnCount()];
    628         int[] positions = mTree.getColumnOrder();
    629         for (int i = 0; i < columns.length; i++) {
    630             TreeColumn treeColumn = mTree.getColumn(i);
    631             LintColumn column = (LintColumn) treeColumn.getData(KEY_COLUMN);
    632             // Workaround for TeeColumn.getWidth() returning 0 in some cases,
    633             // see https://bugs.eclipse.org/341865 for details.
    634             int width = getColumnWidth(column, mTreePainted);
    635             columnEntry.putInteger(getKey(treeColumn), width);
    636             columns[positions[i]] = column;
    637         }
    638 
    639         if (getVisibleColumns() != null) {
    640             IMemento visibleEntry = memento.createChild(KEY_VISIBLE);
    641             for (LintColumn column : getVisibleColumns()) {
    642                 visibleEntry.putBoolean(getKey(column), true);
    643             }
    644         }
    645     }
    646 
    647     private void createColumns() {
    648         LintColumn[] columns = getVisibleColumns();
    649         TableLayout layout = new TableLayout();
    650 
    651         for (int i = 0; i < columns.length; i++) {
    652             LintColumn column = columns[i];
    653             TreeViewerColumn viewerColumn = null;
    654             TreeColumn treeColumn;
    655             viewerColumn = new TreeViewerColumn(mTreeViewer, SWT.NONE);
    656             treeColumn = viewerColumn.getColumn();
    657             treeColumn.setData(KEY_COLUMN, column);
    658             treeColumn.setResizable(true);
    659             treeColumn.addSelectionListener(getHeaderListener());
    660             if (!column.isLeftAligned()) {
    661                 treeColumn.setAlignment(SWT.RIGHT);
    662             }
    663             viewerColumn.setLabelProvider(new LintColumnLabelProvider(column));
    664             treeColumn.setText(column.getColumnHeaderText());
    665             treeColumn.setImage(column.getColumnHeaderImage());
    666             IMemento columnWidths = null;
    667             if (mMemento != null && !mSingleFile) {
    668                 columnWidths = mMemento.getChild(KEY_WIDTHS);
    669             }
    670             int columnWidth = getColumnWidth(column, false);
    671             if (columnWidths != null) {
    672                 columnWidths.putInteger(getKey(column), columnWidth);
    673             }
    674             if (i == 0) {
    675                 // The first column should use layout -weights- to get all the
    676                 // remaining room
    677                 layout.addColumnData(new ColumnWeightData(1, true));
    678             } else if (columnWidth < 0) {
    679                 int defaultColumnWidth = column.getPreferredWidth();
    680                 layout.addColumnData(new ColumnPixelData(defaultColumnWidth, true, true));
    681             } else {
    682                 layout.addColumnData(new ColumnPixelData(columnWidth, true));
    683             }
    684         }
    685         mTreeViewer.getTree().setLayout(layout);
    686         mTree.layout(true);
    687     }
    688 
    689     private int getColumnWidth(LintColumn column, boolean getFromUi) {
    690         Tree tree = mTreeViewer.getTree();
    691         if (getFromUi) {
    692             TreeColumn[] columns = tree.getColumns();
    693             for (int i = 0; i < columns.length; i++) {
    694                 if (column.equals(columns[i].getData(KEY_COLUMN))) {
    695                     return columns[i].getWidth();
    696                 }
    697             }
    698         }
    699         int preferredWidth = -1;
    700         if (mMemento != null && !mSingleFile) {
    701             IMemento columnWidths = mMemento.getChild(KEY_WIDTHS);
    702             if (columnWidths != null) {
    703                 Integer value = columnWidths.getInteger(getKey(column));
    704                 // Make sure we get a useful value
    705                 if (value != null && value.intValue() >= 0)
    706                     preferredWidth = value.intValue();
    707             }
    708         }
    709         if (preferredWidth <= 0) {
    710             preferredWidth = Math.max(column.getPreferredWidth(), 30);
    711         }
    712         return preferredWidth;
    713     }
    714 
    715     private static String getKey(TreeColumn treeColumn) {
    716         return getKey((LintColumn) treeColumn.getData(KEY_COLUMN));
    717     }
    718 
    719     private static String getKey(LintColumn column) {
    720         return column.getClass().getSimpleName();
    721     }
    722 
    723     private LintColumn[] getVisibleColumns() {
    724         if (mVisibleColumns == null) {
    725             if (mSingleFile) {
    726                 // Special mode where we show just lint warnings for a single file:
    727                 // use a hardcoded list of columns, not including path/location etc but
    728                 // including line numbers (which are normally not shown by default).
    729                 mVisibleColumns = new LintColumn[] {
    730                         mMessageColumn, mLineColumn
    731                 };
    732             } else {
    733                 // Generate visible columns based on (a) previously saved window state,
    734                 // and (b) default window visible states provided by the columns themselves
    735                 List<LintColumn> list = new ArrayList<LintColumn>();
    736                 IMemento visibleColumns = null;
    737                 if (mMemento != null) {
    738                     visibleColumns = mMemento.getChild(KEY_VISIBLE);
    739                 }
    740                 for (LintColumn column : mColumns) {
    741                     if (visibleColumns != null) {
    742                         Boolean b = visibleColumns.getBoolean(getKey(column));
    743                         if (b != null && b.booleanValue()) {
    744                             list.add(column);
    745                         }
    746                     } else if (column.visibleByDefault()) {
    747                         list.add(column);
    748                     }
    749                 }
    750                 if (!list.contains(mMessageColumn)) {
    751                     list.add(0, mMessageColumn);
    752                 }
    753                 mVisibleColumns = list.toArray(new LintColumn[list.size()]);
    754             }
    755         }
    756 
    757         return mVisibleColumns;
    758     }
    759 
    760     int getCount(IMarker marker) {
    761         return mContentProvider.getCount(marker);
    762     }
    763 
    764     Issue getIssue(String id) {
    765         return mRegistry.getIssue(id);
    766     }
    767 
    768     Issue getIssue(IMarker marker) {
    769         String id = EclipseLintClient.getId(marker);
    770         return mRegistry.getIssue(id);
    771     }
    772 
    773     Severity getSeverity(Issue issue) {
    774         return mConfiguration.getSeverity(issue);
    775     }
    776 
    777     // ---- Choosing visible columns ----
    778 
    779     public void configureColumns() {
    780         ColumnDialog dialog = new ColumnDialog(getShell(), mColumns, getVisibleColumns());
    781         if (dialog.open() == Window.OK) {
    782             mVisibleColumns = dialog.getSelectedColumns();
    783             // Clear out columns: Must recreate to set the right label provider etc
    784             for (TreeColumn column : mTree.getColumns()) {
    785                 column.dispose();
    786             }
    787             createColumns();
    788             mTreeViewer.setComparator(new TableComparator());
    789             setSortIndicators();
    790             mTreeViewer.refresh();
    791         }
    792     }
    793 
    794     // ---- Table Sorting ----
    795 
    796     private SelectionListener getHeaderListener() {
    797         return new SelectionAdapter() {
    798             @Override
    799             public void widgetSelected(SelectionEvent e) {
    800                 final TreeColumn treeColumn = (TreeColumn) e.widget;
    801                 final LintColumn column = (LintColumn) treeColumn.getData(KEY_COLUMN);
    802 
    803                 try {
    804                     IWorkbenchSiteProgressService progressService = getProgressService();
    805                     if (progressService == null) {
    806                         BusyIndicator.showWhile(getShell().getDisplay(), new Runnable() {
    807                             @Override
    808                             public void run() {
    809                                 resortTable(treeColumn, column,
    810                                         new NullProgressMonitor());
    811                             }
    812                         });
    813                     } else {
    814                         getProgressService().busyCursorWhile(new IRunnableWithProgress() {
    815                             @Override
    816                             public void run(IProgressMonitor monitor) {
    817                                 resortTable(treeColumn, column, monitor);
    818                             }
    819                         });
    820                     }
    821                 } catch (InvocationTargetException e1) {
    822                     AdtPlugin.log(e1, null);
    823                 } catch (InterruptedException e1) {
    824                     return;
    825                 }
    826             }
    827 
    828             private void resortTable(final TreeColumn treeColumn, LintColumn column,
    829                     IProgressMonitor monitor) {
    830                 TableComparator sorter = getTableSorter();
    831                 monitor.beginTask("Sorting", 100);
    832                 monitor.worked(10);
    833                 if (column.equals(sorter.getTopColumn())) {
    834                     sorter.reverseTopPriority();
    835                 } else {
    836                     sorter.setTopPriority(column);
    837                 }
    838                 monitor.worked(15);
    839                 PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
    840                     @Override
    841                     public void run() {
    842                         mTreeViewer.refresh();
    843                         updateDirectionIndicator(treeColumn);
    844                     }
    845                 });
    846                 monitor.done();
    847             }
    848         };
    849     }
    850 
    851     private void setSortIndicators() {
    852         LintColumn top = getTableSorter().getTopColumn();
    853         TreeColumn[] columns = mTreeViewer.getTree().getColumns();
    854         for (int i = 0; i < columns.length; i++) {
    855             TreeColumn column = columns[i];
    856             if (column.getData(KEY_COLUMN).equals(top)) {
    857                 updateDirectionIndicator(column);
    858                 return;
    859             }
    860         }
    861     }
    862 
    863     private void updateDirectionIndicator(TreeColumn column) {
    864         Tree tree = mTreeViewer.getTree();
    865         tree.setSortColumn(column);
    866         if (getTableSorter().isAscending()) {
    867             tree.setSortDirection(SWT.UP);
    868         } else {
    869             tree.setSortDirection(SWT.DOWN);
    870         }
    871     }
    872 
    873     private TableComparator getTableSorter() {
    874         return (TableComparator) mTreeViewer.getComparator();
    875     }
    876 
    877     /** Comparator used to sort the {@link LintList} tree.
    878      * <p>
    879      * This code is simplified from similar code in
    880      *    org.eclipse.ui.views.markers.internal.TableComparator
    881      */
    882     private class TableComparator extends ViewerComparator {
    883         private int[] mPriorities;
    884         private boolean[] mDirections;
    885         private int[] mDefaultPriorities;
    886         private boolean[] mDefaultDirections;
    887 
    888         private TableComparator() {
    889             int[] defaultPriorities = new int[mColumns.length];
    890             for (int i = 0; i < defaultPriorities.length; i++) {
    891                 defaultPriorities[i] = i;
    892             }
    893             mPriorities = defaultPriorities;
    894 
    895             boolean[] directions = new boolean[mColumns.length];
    896             for (int i = 0; i < directions.length; i++) {
    897                 directions[i] = mColumns[i].isAscending();
    898             }
    899             mDirections = directions;
    900 
    901             mDefaultPriorities = new int[defaultPriorities.length];
    902             System.arraycopy(defaultPriorities, 0, this.mDefaultPriorities, 0,
    903                     defaultPriorities.length);
    904             mDefaultDirections = new boolean[directions.length];
    905             System.arraycopy(directions, 0, this.mDefaultDirections, 0, directions.length);
    906         }
    907 
    908         private void resetState() {
    909             System.arraycopy(mDefaultPriorities, 0, mPriorities, 0, mPriorities.length);
    910             System.arraycopy(mDefaultDirections, 0, mDirections, 0, mDirections.length);
    911         }
    912 
    913         private void reverseTopPriority() {
    914             mDirections[mPriorities[0]] = !mDirections[mPriorities[0]];
    915         }
    916 
    917         private void setTopPriority(LintColumn property) {
    918             for (int i = 0; i < mColumns.length; i++) {
    919                 if (mColumns[i].equals(property)) {
    920                     setTopPriority(i);
    921                     return;
    922                 }
    923             }
    924         }
    925 
    926         private void setTopPriority(int priority) {
    927             if (priority < 0 || priority >= mPriorities.length) {
    928                 return;
    929             }
    930             int index = -1;
    931             for (int i = 0; i < mPriorities.length; i++) {
    932                 if (mPriorities[i] == priority) {
    933                     index = i;
    934                 }
    935             }
    936             if (index == -1) {
    937                 resetState();
    938                 return;
    939             }
    940             // shift the array
    941             for (int i = index; i > 0; i--) {
    942                 mPriorities[i] = mPriorities[i - 1];
    943             }
    944             mPriorities[0] = priority;
    945             mDirections[priority] = mDefaultDirections[priority];
    946         }
    947 
    948         private boolean isAscending() {
    949             return mDirections[mPriorities[0]];
    950         }
    951 
    952         private int getTopPriority() {
    953             return mPriorities[0];
    954         }
    955 
    956         private LintColumn getTopColumn() {
    957             return mColumns[getTopPriority()];
    958         }
    959 
    960         @Override
    961         public int compare(Viewer viewer, Object e1, Object e2) {
    962             return compare((IMarker) e1, (IMarker) e2, 0, true);
    963         }
    964 
    965         private int compare(IMarker marker1, IMarker marker2, int depth,
    966                 boolean continueSearching) {
    967             if (depth >= mPriorities.length) {
    968                 return 0;
    969             }
    970             int column = mPriorities[depth];
    971             LintColumn property = mColumns[column];
    972             int result = property.compare(marker1, marker2);
    973             if (result == 0 && continueSearching) {
    974                 return compare(marker1, marker2, depth + 1, continueSearching);
    975             }
    976             return result * (mDirections[column] ? 1 : -1);
    977         }
    978     }
    979 }
    980