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 
LintList(IWorkbenchPartSite site, Composite parent, IMemento memento, boolean singleFile)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 
updateColumnWidths()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 
setResources(List<? extends IResource> resources)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 */
selectFirst()265     public void selectFirst() {
266         if (mTree.getItemCount() > 0) {
267             mTree.select(mTree.getItem(0));
268         }
269     }
270 
getMarkers()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 
getErrorCount()293     public int getErrorCount() {
294         return mErrorCount;
295     }
296 
getWarningCount()297     public int getWarningCount() {
298         return mWarningCount;
299     }
300 
301     @Override
checkSubclass()302     protected void checkSubclass() {
303         // Disable the check that prevents subclassing of SWT components
304     }
305 
addSelectionListener(SelectionListener listener)306     public void addSelectionListener(SelectionListener listener) {
307         mTree.addSelectionListener(listener);
308     }
309 
refresh()310     public void refresh() {
311         mTreeViewer.refresh();
312     }
313 
getSelectedMarkers()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
dispose()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
getElements(Object inputElement)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
hasChildren(Object element)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
getChildren(Object parentElement)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
getParent(Object element)404         public Object getParent(Object element) {
405             return null;
406         }
407 
getCount(IMarker marker)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 
getTopMarkers()419         IMarker[] getTopMarkers() {
420             return mTopLevels;
421         }
422     }
423 
424     private class LintColumnLabelProvider extends StyledCellLabelProvider {
425         private LintColumn mColumn;
426 
LintColumnLabelProvider(LintColumn column)427         LintColumnLabelProvider(LintColumn column) {
428             mColumn = column;
429         }
430 
431         @Override
update(ViewerCell cell)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 
getTreeViewer()447     TreeViewer getTreeViewer() {
448         return mTreeViewer;
449     }
450 
getTree()451     Tree getTree() {
452         return mTree;
453     }
454 
455     // ---- Implements IResourceChangeListener ----
456 
457     @Override
resourceChanged(IResourceChangeEvent event)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
controlMoved(ControlEvent e)484     public void controlMoved(ControlEvent e) {
485     }
486 
487     @Override
controlResized(ControlEvent e)488     public void controlResized(ControlEvent e) {
489         updateColumnWidths();
490     }
491 
492     // ---- Updating Markers ----
493 
cancelJobs()494     private void cancelJobs() {
495         mUpdateMarkersJob.cancel();
496     }
497 
getProgressService()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 {
UpdateMarkersJob()509         UpdateMarkersJob() {
510             super("Updating Lint Markers");
511             setSystem(true);
512         }
513 
514         @Override
runInUIThread(IProgressMonitor monitor)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
shouldRun()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
belongsTo(Object family)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      */
getResources()588     public List<? extends IResource> getResources() {
589         return mResources;
590     }
591 
592     /** Expands all nodes */
expandAll()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 */
collapseAll()611     public void collapseAll() {
612         mTreeViewer.collapseAll();
613         mExpandedIds = null;
614     }
615 
616     // ---- Column Persistence ----
617 
saveState(IMemento memento)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 
createColumns()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 
getColumnWidth(LintColumn column, boolean getFromUi)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 
getKey(TreeColumn treeColumn)715     private static String getKey(TreeColumn treeColumn) {
716         return getKey((LintColumn) treeColumn.getData(KEY_COLUMN));
717     }
718 
getKey(LintColumn column)719     private static String getKey(LintColumn column) {
720         return column.getClass().getSimpleName();
721     }
722 
getVisibleColumns()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 
getCount(IMarker marker)760     int getCount(IMarker marker) {
761         return mContentProvider.getCount(marker);
762     }
763 
getIssue(String id)764     Issue getIssue(String id) {
765         return mRegistry.getIssue(id);
766     }
767 
getIssue(IMarker marker)768     Issue getIssue(IMarker marker) {
769         String id = EclipseLintClient.getId(marker);
770         return mRegistry.getIssue(id);
771     }
772 
getSeverity(Issue issue)773     Severity getSeverity(Issue issue) {
774         return mConfiguration.getSeverity(issue);
775     }
776 
777     // ---- Choosing visible columns ----
778 
configureColumns()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 
getHeaderListener()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