fix SingleInspectionProfilePanelTest
[idea/community.git] / platform / lang-impl / src / com / intellij / profile / codeInspection / ui / inspectionsTree / InspectionsConfigTreeTable.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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.intellij.profile.codeInspection.ui.inspectionsTree;
17
18 import com.intellij.codeHighlighting.HighlightDisplayLevel;
19 import com.intellij.codeInsight.daemon.HighlightDisplayKey;
20 import com.intellij.codeInspection.ex.InspectionProfileImpl;
21 import com.intellij.codeInspection.ex.ScopeToolState;
22 import com.intellij.codeInspection.ex.ToolsImpl;
23 import com.intellij.ide.IdeTooltip;
24 import com.intellij.ide.IdeTooltipManager;
25 import com.intellij.lang.annotation.HighlightSeverity;
26 import com.intellij.openapi.Disposable;
27 import com.intellij.openapi.application.ModalityState;
28 import com.intellij.openapi.diagnostic.Logger;
29 import com.intellij.openapi.project.Project;
30 import com.intellij.openapi.util.Comparing;
31 import com.intellij.openapi.util.SystemInfo;
32 import com.intellij.openapi.util.text.StringUtil;
33 import com.intellij.profile.codeInspection.ui.InspectionsAggregationUtil;
34 import com.intellij.profile.codeInspection.ui.SingleInspectionProfilePanel;
35 import com.intellij.profile.codeInspection.ui.ToolDescriptors;
36 import com.intellij.profile.codeInspection.ui.table.ScopesAndSeveritiesTable;
37 import com.intellij.profile.codeInspection.ui.table.ThreeStateCheckBoxRenderer;
38 import com.intellij.ui.DoubleClickListener;
39 import com.intellij.ui.treeStructure.treetable.TreeTable;
40 import com.intellij.ui.treeStructure.treetable.TreeTableModel;
41 import com.intellij.ui.treeStructure.treetable.TreeTableTree;
42 import com.intellij.util.Alarm;
43 import com.intellij.util.NullableFunction;
44 import com.intellij.util.containers.ContainerUtil;
45 import com.intellij.util.containers.HashSet;
46 import com.intellij.util.ui.JBUI;
47 import com.intellij.util.ui.TextTransferable;
48 import com.intellij.util.ui.table.IconTableCellRenderer;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51
52 import javax.swing.*;
53 import javax.swing.table.AbstractTableModel;
54 import javax.swing.table.TableColumn;
55 import javax.swing.tree.DefaultTreeModel;
56 import javax.swing.tree.TreeNode;
57 import javax.swing.tree.TreePath;
58 import java.awt.*;
59 import java.awt.datatransfer.Transferable;
60 import java.awt.event.*;
61 import java.util.*;
62 import java.util.List;
63
64 /**
65  * @author Dmitry Batkovich
66  */
67 public class InspectionsConfigTreeTable extends TreeTable {
68   private final static Logger LOG = Logger.getInstance(InspectionsConfigTreeTable.class);
69
70   private final static int TREE_COLUMN = 0;
71   private final static int SEVERITIES_COLUMN = 1;
72   private final static int IS_ENABLED_COLUMN = 2;
73
74   public static int getAdditionalPadding() {
75     return SystemInfo.isMac ? 10 : 0;
76   }
77
78   public static InspectionsConfigTreeTable create(final InspectionsConfigTreeTableSettings settings, Disposable parentDisposable) {
79     return new InspectionsConfigTreeTable(new InspectionsConfigTreeTableModel(settings, parentDisposable));
80   }
81
82   public InspectionsConfigTreeTable(final InspectionsConfigTreeTableModel model) {
83     super(model);
84
85     final TableColumn severitiesColumn = getColumnModel().getColumn(SEVERITIES_COLUMN);
86     severitiesColumn.setCellRenderer(new IconTableCellRenderer<Icon>() {
87
88       @Override
89       public Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean focus, int row, int column) {
90         Component component = super.getTableCellRendererComponent(table, value, false, focus, row, column);
91         Color bg = selected ? table.getSelectionBackground() : table.getBackground();
92         component.setBackground(bg);
93         ((JLabel) component).setText("");
94         return component;
95       }
96
97       @Nullable
98       @Override
99       protected Icon getIcon(@NotNull Icon value, JTable table, int row) {
100         return value;
101       }
102     });
103     severitiesColumn.setMaxWidth(JBUI.scale(20));
104
105     final TableColumn isEnabledColumn = getColumnModel().getColumn(IS_ENABLED_COLUMN);
106     isEnabledColumn.setMaxWidth(JBUI.scale(20 + getAdditionalPadding()));
107     isEnabledColumn.setCellRenderer(new ThreeStateCheckBoxRenderer());
108     isEnabledColumn.setCellEditor(new ThreeStateCheckBoxRenderer());
109
110     addMouseMotionListener(new MouseAdapter() {
111       @Override
112       public void mouseMoved(final MouseEvent e) {
113         final Point point = e.getPoint();
114         final int column = columnAtPoint(point);
115         if (column != SEVERITIES_COLUMN) {
116           return;
117         }
118         final int row = rowAtPoint(point);
119         final Object maybeIcon = getModel().getValueAt(row, column);
120         if (maybeIcon instanceof MultiScopeSeverityIcon) {
121           final MultiScopeSeverityIcon icon = (MultiScopeSeverityIcon)maybeIcon;
122           final LinkedHashMap<String, HighlightDisplayLevel> scopeToAverageSeverityMap =
123             icon.getScopeToAverageSeverityMap();
124           final JComponent component;
125           if (scopeToAverageSeverityMap.size() == 1 &&
126               icon.getDefaultScopeName().equals(ContainerUtil.getFirstItem(scopeToAverageSeverityMap.keySet()))) {
127             final HighlightDisplayLevel level = ContainerUtil.getFirstItem(scopeToAverageSeverityMap.values());
128             final JLabel label = new JLabel();
129             label.setIcon(level.getIcon());
130             label.setText(SingleInspectionProfilePanel.renderSeverity(level.getSeverity()));
131             component = label;
132           } else {
133             component = new ScopesAndSeveritiesHintTable(scopeToAverageSeverityMap, icon.getDefaultScopeName());
134           }
135           IdeTooltipManager.getInstance().show(
136             new IdeTooltip(InspectionsConfigTreeTable.this, point, component), false);
137         }
138       }
139     });
140
141     new DoubleClickListener() {
142       @Override
143       protected boolean onDoubleClick(MouseEvent event) {
144         final TreePath path = getTree().getPathForRow(getTree().getLeadSelectionRow());
145         if (path != null) {
146           final InspectionConfigTreeNode node = (InspectionConfigTreeNode)path.getLastPathComponent();
147           if (node.isLeaf()) {
148             model.swapInspectionEnableState();
149           }
150         }
151         return true;
152       }
153     }.installOn(this);
154
155     setTransferHandler(new TransferHandler() {
156       @Nullable
157       @Override
158       protected Transferable createTransferable(JComponent c) {
159         final TreePath path = getTree().getPathForRow(getTree().getLeadSelectionRow());
160         if (path != null) {
161           return new TextTransferable(StringUtil.join(ContainerUtil.mapNotNull(path.getPath(),
162                                                                                (NullableFunction<Object, String>)o -> o == path.getPath()[0] ? null : o.toString()), " | "));
163         }
164         return null;
165       }
166
167       @Override
168       public int getSourceActions(JComponent c) {
169         return COPY;
170       }
171     });
172
173     getTableHeader().setReorderingAllowed(false);
174     registerKeyboardAction(new ActionListener() {
175                              public void actionPerformed(ActionEvent e) {
176                                model.swapInspectionEnableState();
177                                updateUI();
178                              }
179                            }, KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), JComponent.WHEN_FOCUSED);
180
181     getEmptyText().setText("No enabled inspections available");
182   }
183
184   public abstract static class InspectionsConfigTreeTableSettings {
185     private final TreeNode myRoot;
186     private final Project myProject;
187
188     public InspectionsConfigTreeTableSettings(final TreeNode root, final Project project) {
189       myRoot = root;
190       myProject = project;
191     }
192
193     public TreeNode getRoot() {
194       return myRoot;
195     }
196
197     public Project getProject() {
198       return myProject;
199     }
200
201     protected abstract InspectionProfileImpl getInspectionProfile();
202
203     protected abstract void onChanged(InspectionConfigTreeNode node);
204
205     public abstract void updateRightPanel();
206   }
207
208   private static class InspectionsConfigTreeTableModel extends DefaultTreeModel implements TreeTableModel {
209
210     private final InspectionsConfigTreeTableSettings mySettings;
211     private final Runnable myUpdateRunnable;
212     private TreeTable myTreeTable;
213
214     private Alarm myUpdateAlarm;
215
216     public InspectionsConfigTreeTableModel(final InspectionsConfigTreeTableSettings settings, Disposable parentDisposable) {
217       super(settings.getRoot());
218       mySettings = settings;
219       myUpdateRunnable = () -> {
220         settings.updateRightPanel();
221         ((AbstractTableModel)myTreeTable.getModel()).fireTableDataChanged();
222       };
223       myUpdateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, parentDisposable);
224     }
225
226     @Override
227     public int getColumnCount() {
228       return 3;
229     }
230
231     @Nullable
232     @Override
233     public String getColumnName(final int column) {
234       return null;
235     }
236
237     @Override
238     public Class getColumnClass(final int column) {
239       switch (column) {
240         case TREE_COLUMN:
241           return TreeTableModel.class;
242         case SEVERITIES_COLUMN:
243           return Icon.class;
244         case IS_ENABLED_COLUMN:
245           return Boolean.class;
246       }
247       throw new IllegalArgumentException();
248     }
249
250     @Nullable
251     @Override
252     public Object getValueAt(final Object node, final int column) {
253       if (column == TREE_COLUMN) {
254         return null;
255       }
256       final InspectionConfigTreeNode treeNode = (InspectionConfigTreeNode)node;
257       final List<HighlightDisplayKey> inspectionsKeys = InspectionsAggregationUtil.getInspectionsKeys(treeNode);
258       if (column == SEVERITIES_COLUMN) {
259         final MultiColoredHighlightSeverityIconSink sink = new MultiColoredHighlightSeverityIconSink();
260         for (final HighlightDisplayKey selectedInspectionsNode : inspectionsKeys) {
261           final String toolId = selectedInspectionsNode.toString();
262           if (mySettings.getInspectionProfile().getTools(toolId, mySettings.getProject()).isEnabled()) {
263             sink.put(mySettings.getInspectionProfile().getToolDefaultState(toolId, mySettings.getProject()),
264                      mySettings.getInspectionProfile().getNonDefaultTools(toolId, mySettings.getProject()));
265           }
266         }
267         return sink.constructIcon(mySettings.getInspectionProfile());
268       } else if (column == IS_ENABLED_COLUMN) {
269         return isEnabled(inspectionsKeys);
270       }
271       throw new IllegalArgumentException();
272     }
273
274     @Nullable
275     private Boolean isEnabled(final List<HighlightDisplayKey> selectedInspectionsNodes) {
276       Boolean isPreviousEnabled = null;
277       for (final HighlightDisplayKey key : selectedInspectionsNodes) {
278         final ToolsImpl tools = mySettings.getInspectionProfile().getTools(key.toString(), mySettings.getProject());
279         for (final ScopeToolState state : tools.getTools()) {
280           final boolean enabled = state.isEnabled();
281           if (isPreviousEnabled == null) {
282             isPreviousEnabled = enabled;
283           }
284           else if (!isPreviousEnabled.equals(enabled)) {
285             return null;
286           }
287         }
288       }
289       return isPreviousEnabled;
290     }
291
292     @Override
293     public boolean isCellEditable(final Object node, final int column) {
294       return column == IS_ENABLED_COLUMN;
295     }
296
297     @Override
298     public void setValueAt(final Object aValue, final Object node, final int column) {
299       LOG.assertTrue(column == IS_ENABLED_COLUMN);
300       if (aValue == null) {
301         return;
302       }
303       final boolean doEnable = (Boolean) aValue;
304       final InspectionProfileImpl profile = mySettings.getInspectionProfile();
305       for (final InspectionConfigTreeNode aNode : InspectionsAggregationUtil.getInspectionsNodes((InspectionConfigTreeNode)node)) {
306         setToolEnabled(doEnable, profile, aNode.getKey());
307         aNode.dropCache();
308         mySettings.onChanged(aNode);
309       }
310       updateRightPanel();
311     }
312
313     public void swapInspectionEnableState() {
314       LOG.assertTrue(myTreeTable != null);
315
316       Boolean state = null;
317       final HashSet<HighlightDisplayKey> tools = new HashSet<>();
318       final List<InspectionConfigTreeNode> nodes = new ArrayList<>();
319
320       for (TreePath selectionPath : myTreeTable.getTree().getSelectionPaths()) {
321         final InspectionConfigTreeNode node = (InspectionConfigTreeNode)selectionPath.getLastPathComponent();
322         collectInspectionFromNodes(node, tools, nodes);
323       }
324
325       final int[] selectedRows = myTreeTable.getSelectedRows();
326       for (int selectedRow : selectedRows) {
327         final Boolean value = (Boolean)myTreeTable.getValueAt(selectedRow, IS_ENABLED_COLUMN);
328         if (state == null) {
329           state = value;
330         }
331         else if (!state.equals(value)) {
332           state = null;
333           break;
334         }
335       }
336       final boolean newState = !Boolean.TRUE.equals(state);
337
338       final InspectionProfileImpl profile = mySettings.getInspectionProfile();
339       for (HighlightDisplayKey tool : tools) {
340         setToolEnabled(newState, profile, tool);
341       }
342
343       for (InspectionConfigTreeNode node : nodes) {
344         node.dropCache();
345         mySettings.onChanged(node);
346       }
347
348       updateRightPanel();
349     }
350
351     private void updateRightPanel() {
352       if (myTreeTable != null) {
353         if (!myUpdateAlarm.isDisposed()) {
354           myUpdateAlarm.cancelAllRequests();
355           myUpdateAlarm.addRequest(myUpdateRunnable, 10, ModalityState.stateForComponent(myTreeTable));
356         }
357       }
358     }
359
360     private void setToolEnabled(boolean newState, InspectionProfileImpl profile, HighlightDisplayKey tool) {
361       final String toolId = tool.toString();
362       if (newState) {
363         profile.enableTool(toolId, mySettings.getProject());
364       }
365       else {
366         profile.disableTool(toolId, mySettings.getProject());
367       }
368       for (ScopeToolState scopeToolState : profile.getTools(toolId, mySettings.getProject()).getTools()) {
369         scopeToolState.setEnabled(newState);
370       }
371     }
372
373     private static void collectInspectionFromNodes(final InspectionConfigTreeNode node,
374                                                    final Set<HighlightDisplayKey> tools,
375                                                    final List<InspectionConfigTreeNode> nodes) {
376       if (node == null) {
377         return;
378       }
379       nodes.add(node);
380
381       final ToolDescriptors descriptors = node.getDescriptors();
382       if (descriptors == null) {
383         for (int i = 0; i < node.getChildCount(); i++) {
384           collectInspectionFromNodes((InspectionConfigTreeNode)node.getChildAt(i), tools, nodes);
385         }
386       } else {
387         final HighlightDisplayKey key = descriptors.getDefaultDescriptor().getKey();
388         tools.add(key);
389       }
390     }
391
392     @Override
393     public void setTree(final JTree tree) {
394       myTreeTable = ((TreeTableTree)tree).getTreeTable();
395     }
396   }
397
398   private static class SeverityAndOccurrences {
399     private HighlightSeverity myPrimarySeverity;
400     private final Map<String, HighlightSeverity> myOccurrences = new HashMap<>();
401
402     public void setSeverityToMixed() {
403       myPrimarySeverity = ScopesAndSeveritiesTable.MIXED_FAKE_SEVERITY;
404     }
405
406     public SeverityAndOccurrences incOccurrences(final String toolName, final HighlightSeverity severity) {
407       if (myPrimarySeverity == null) {
408         myPrimarySeverity = severity;
409       } else if (!Comparing.equal(severity, myPrimarySeverity)) {
410         myPrimarySeverity = ScopesAndSeveritiesTable.MIXED_FAKE_SEVERITY;
411       }
412       myOccurrences.put(toolName, severity);
413       return this;
414     }
415
416     public HighlightSeverity getPrimarySeverity() {
417       return myPrimarySeverity;
418     }
419
420     public int getOccurrencesSize() {
421       return myOccurrences.size();
422     }
423
424     public Map<String, HighlightSeverity> getOccurrences() {
425       return myOccurrences;
426     }
427   }
428
429   private static class MultiColoredHighlightSeverityIconSink {
430
431
432     private final Map<String, SeverityAndOccurrences> myScopeToAverageSeverityMap = new HashMap<>();
433
434     private String myDefaultScopeName;
435
436     public Icon constructIcon(final InspectionProfileImpl inspectionProfile) {
437       final Map<String, HighlightSeverity> computedSeverities = computeSeverities(inspectionProfile);
438
439       if (computedSeverities == null) {
440         return null;
441       }
442
443       boolean allScopesHasMixedSeverity = true;
444       for (HighlightSeverity severity : computedSeverities.values()) {
445         if (!severity.equals(ScopesAndSeveritiesTable.MIXED_FAKE_SEVERITY)) {
446           allScopesHasMixedSeverity = false;
447           break;
448         }
449       }
450       return allScopesHasMixedSeverity
451              ? ScopesAndSeveritiesTable.MIXED_FAKE_LEVEL.getIcon()
452              : new MultiScopeSeverityIcon(computedSeverities, myDefaultScopeName, inspectionProfile);
453     }
454
455     @Nullable
456     private Map<String, HighlightSeverity> computeSeverities(final InspectionProfileImpl inspectionProfile) {
457       if (myScopeToAverageSeverityMap.isEmpty()) {
458         return null;
459       }
460       final Map<String, HighlightSeverity> result = new HashMap<>();
461       final Map.Entry<String, SeverityAndOccurrences> entry = ContainerUtil.getFirstItem(myScopeToAverageSeverityMap.entrySet());
462       result.put(entry.getKey(), entry.getValue().getPrimarySeverity());
463       if (myScopeToAverageSeverityMap.size() == 1) {
464         return result;
465       }
466
467       final SeverityAndOccurrences defaultSeveritiesAndOccurrences = myScopeToAverageSeverityMap.get(myDefaultScopeName);
468       if (defaultSeveritiesAndOccurrences == null) {
469         for (Map.Entry<String, SeverityAndOccurrences> e: myScopeToAverageSeverityMap.entrySet()) {
470           final HighlightSeverity primarySeverity = e.getValue().getPrimarySeverity();
471           if (primarySeverity != null) {
472             result.put(e.getKey(), primarySeverity);
473           }
474         }
475         return result;
476       }
477       final int allInspectionsCount = defaultSeveritiesAndOccurrences.getOccurrencesSize();
478       final Map<String, HighlightSeverity> allScopes = defaultSeveritiesAndOccurrences.getOccurrences();
479       for (String currentScope : myScopeToAverageSeverityMap.keySet()) {
480         final SeverityAndOccurrences currentSeverityAndOccurrences = myScopeToAverageSeverityMap.get(currentScope);
481         if (currentSeverityAndOccurrences == null) {
482           continue;
483         }
484         final HighlightSeverity currentSeverity = currentSeverityAndOccurrences.getPrimarySeverity();
485         if (currentSeverity == ScopesAndSeveritiesTable.MIXED_FAKE_SEVERITY ||
486             currentSeverityAndOccurrences.getOccurrencesSize() == allInspectionsCount ||
487             myDefaultScopeName.equals(currentScope)) {
488           result.put(currentScope, currentSeverity);
489         }
490         else {
491           Set<String> toolsToCheck = ContainerUtil.newHashSet(allScopes.keySet());
492           toolsToCheck.removeAll(currentSeverityAndOccurrences.getOccurrences().keySet());
493           boolean doContinue = false;
494           final Map<String, HighlightSeverity> lowerScopeOccurrences = myScopeToAverageSeverityMap.get(myDefaultScopeName).getOccurrences();
495           for (String toolName : toolsToCheck) {
496             final HighlightSeverity currentToolSeverity = lowerScopeOccurrences.get(toolName);
497             if (currentToolSeverity != null) {
498               if (!currentSeverity.equals(currentToolSeverity)) {
499                 result.put(currentScope, ScopesAndSeveritiesTable.MIXED_FAKE_SEVERITY);
500                 doContinue = true;
501                 break;
502               }
503             }
504           }
505           if (doContinue) {
506             continue;
507           }
508           result.put(currentScope, currentSeverity);
509         }
510       }
511
512       return result;
513     }
514
515     public void put(@NotNull final ScopeToolState defaultState, @NotNull final List<ScopeToolState> nonDefault) {
516       putOne(defaultState);
517       if (myDefaultScopeName == null) {
518         myDefaultScopeName = defaultState.getScopeName();
519       }
520       for (final ScopeToolState scopeToolState : nonDefault) {
521         putOne(scopeToolState);
522       }
523     }
524
525     private void putOne(final ScopeToolState state) {
526       if (!state.isEnabled()) {
527         return;
528       }
529       final Icon icon = state.getLevel().getIcon();
530       final String scopeName = state.getScopeName();
531       if (icon instanceof HighlightDisplayLevel.ColoredIcon) {
532         final SeverityAndOccurrences severityAndOccurrences = myScopeToAverageSeverityMap.get(scopeName);
533         final String inspectionName = state.getTool().getShortName();
534         if (severityAndOccurrences == null) {
535           myScopeToAverageSeverityMap.put(scopeName, new SeverityAndOccurrences().incOccurrences(inspectionName, state.getLevel().getSeverity()));
536         } else {
537           severityAndOccurrences.incOccurrences(inspectionName, state.getLevel().getSeverity());
538         }
539       }
540     }
541   }
542 }