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