2 * Copyright 2000-2016 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.profile.codeInspection.ui.inspectionsTree;
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;
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;
59 import java.awt.datatransfer.Transferable;
60 import java.awt.event.*;
62 import java.util.List;
65 * @author Dmitry Batkovich
67 public class InspectionsConfigTreeTable extends TreeTable {
68 private final static Logger LOG = Logger.getInstance(InspectionsConfigTreeTable.class);
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;
74 public static int getAdditionalPadding() {
75 return SystemInfo.isMac ? 10 : 0;
78 public static InspectionsConfigTreeTable create(final InspectionsConfigTreeTableSettings settings, Disposable parentDisposable) {
79 return new InspectionsConfigTreeTable(new InspectionsConfigTreeTableModel(settings, parentDisposable));
82 public InspectionsConfigTreeTable(final InspectionsConfigTreeTableModel model) {
85 final TableColumn severitiesColumn = getColumnModel().getColumn(SEVERITIES_COLUMN);
86 severitiesColumn.setCellRenderer(new IconTableCellRenderer<Icon>() {
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("");
99 protected Icon getIcon(@NotNull Icon value, JTable table, int row) {
103 severitiesColumn.setMaxWidth(JBUI.scale(20));
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());
110 addMouseMotionListener(new MouseAdapter() {
112 public void mouseMoved(final MouseEvent e) {
113 final Point point = e.getPoint();
114 final int column = columnAtPoint(point);
115 if (column != SEVERITIES_COLUMN) {
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()));
133 component = new ScopesAndSeveritiesHintTable(scopeToAverageSeverityMap, icon.getDefaultScopeName());
135 IdeTooltipManager.getInstance().show(
136 new IdeTooltip(InspectionsConfigTreeTable.this, point, component), false);
141 new DoubleClickListener() {
143 protected boolean onDoubleClick(MouseEvent event) {
144 final TreePath path = getTree().getPathForRow(getTree().getLeadSelectionRow());
146 final InspectionConfigTreeNode node = (InspectionConfigTreeNode)path.getLastPathComponent();
148 model.swapInspectionEnableState();
155 setTransferHandler(new TransferHandler() {
158 protected Transferable createTransferable(JComponent c) {
159 final TreePath path = getTree().getPathForRow(getTree().getLeadSelectionRow());
161 return new TextTransferable(StringUtil.join(ContainerUtil.mapNotNull(path.getPath(),
162 (NullableFunction<Object, String>)o -> o == path.getPath()[0] ? null : o.toString()), " | "));
168 public int getSourceActions(JComponent c) {
173 getTableHeader().setReorderingAllowed(false);
174 registerKeyboardAction(new ActionListener() {
175 public void actionPerformed(ActionEvent e) {
176 model.swapInspectionEnableState();
179 }, KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), JComponent.WHEN_FOCUSED);
181 getEmptyText().setText("No enabled inspections available");
184 public abstract static class InspectionsConfigTreeTableSettings {
185 private final TreeNode myRoot;
186 private final Project myProject;
188 public InspectionsConfigTreeTableSettings(final TreeNode root, final Project project) {
193 public TreeNode getRoot() {
197 public Project getProject() {
201 protected abstract InspectionProfileImpl getInspectionProfile();
203 protected abstract void onChanged(InspectionConfigTreeNode node);
205 public abstract void updateRightPanel();
208 private static class InspectionsConfigTreeTableModel extends DefaultTreeModel implements TreeTableModel {
210 private final InspectionsConfigTreeTableSettings mySettings;
211 private final Runnable myUpdateRunnable;
212 private TreeTable myTreeTable;
214 private Alarm myUpdateAlarm;
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();
223 myUpdateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, parentDisposable);
227 public int getColumnCount() {
233 public String getColumnName(final int column) {
238 public Class getColumnClass(final int column) {
241 return TreeTableModel.class;
242 case SEVERITIES_COLUMN:
244 case IS_ENABLED_COLUMN:
245 return Boolean.class;
247 throw new IllegalArgumentException();
252 public Object getValueAt(final Object node, final int column) {
253 if (column == TREE_COLUMN) {
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()));
267 return sink.constructIcon(mySettings.getInspectionProfile());
268 } else if (column == IS_ENABLED_COLUMN) {
269 return isEnabled(inspectionsKeys);
271 throw new IllegalArgumentException();
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;
284 else if (!isPreviousEnabled.equals(enabled)) {
289 return isPreviousEnabled;
293 public boolean isCellEditable(final Object node, final int column) {
294 return column == IS_ENABLED_COLUMN;
298 public void setValueAt(final Object aValue, final Object node, final int column) {
299 LOG.assertTrue(column == IS_ENABLED_COLUMN);
300 if (aValue == null) {
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());
308 mySettings.onChanged(aNode);
313 public void swapInspectionEnableState() {
314 LOG.assertTrue(myTreeTable != null);
316 Boolean state = null;
317 final HashSet<HighlightDisplayKey> tools = new HashSet<>();
318 final List<InspectionConfigTreeNode> nodes = new ArrayList<>();
320 for (TreePath selectionPath : myTreeTable.getTree().getSelectionPaths()) {
321 final InspectionConfigTreeNode node = (InspectionConfigTreeNode)selectionPath.getLastPathComponent();
322 collectInspectionFromNodes(node, tools, nodes);
325 final int[] selectedRows = myTreeTable.getSelectedRows();
326 for (int selectedRow : selectedRows) {
327 final Boolean value = (Boolean)myTreeTable.getValueAt(selectedRow, IS_ENABLED_COLUMN);
331 else if (!state.equals(value)) {
336 final boolean newState = !Boolean.TRUE.equals(state);
338 final InspectionProfileImpl profile = mySettings.getInspectionProfile();
339 for (HighlightDisplayKey tool : tools) {
340 setToolEnabled(newState, profile, tool);
343 for (InspectionConfigTreeNode node : nodes) {
345 mySettings.onChanged(node);
351 private void updateRightPanel() {
352 if (myTreeTable != null) {
353 if (!myUpdateAlarm.isDisposed()) {
354 myUpdateAlarm.cancelAllRequests();
355 myUpdateAlarm.addRequest(myUpdateRunnable, 10, ModalityState.stateForComponent(myTreeTable));
360 private void setToolEnabled(boolean newState, InspectionProfileImpl profile, HighlightDisplayKey tool) {
361 final String toolId = tool.toString();
363 profile.enableTool(toolId, mySettings.getProject());
366 profile.disableTool(toolId, mySettings.getProject());
368 for (ScopeToolState scopeToolState : profile.getTools(toolId, mySettings.getProject()).getTools()) {
369 scopeToolState.setEnabled(newState);
373 private static void collectInspectionFromNodes(final InspectionConfigTreeNode node,
374 final Set<HighlightDisplayKey> tools,
375 final List<InspectionConfigTreeNode> nodes) {
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);
387 final HighlightDisplayKey key = descriptors.getDefaultDescriptor().getKey();
393 public void setTree(final JTree tree) {
394 myTreeTable = ((TreeTableTree)tree).getTreeTable();
398 private static class SeverityAndOccurrences {
399 private HighlightSeverity myPrimarySeverity;
400 private final Map<String, HighlightSeverity> myOccurrences = new HashMap<>();
402 public void setSeverityToMixed() {
403 myPrimarySeverity = ScopesAndSeveritiesTable.MIXED_FAKE_SEVERITY;
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;
412 myOccurrences.put(toolName, severity);
416 public HighlightSeverity getPrimarySeverity() {
417 return myPrimarySeverity;
420 public int getOccurrencesSize() {
421 return myOccurrences.size();
424 public Map<String, HighlightSeverity> getOccurrences() {
425 return myOccurrences;
429 private static class MultiColoredHighlightSeverityIconSink {
432 private final Map<String, SeverityAndOccurrences> myScopeToAverageSeverityMap = new HashMap<>();
434 private String myDefaultScopeName;
436 public Icon constructIcon(final InspectionProfileImpl inspectionProfile) {
437 final Map<String, HighlightSeverity> computedSeverities = computeSeverities(inspectionProfile);
439 if (computedSeverities == null) {
443 boolean allScopesHasMixedSeverity = true;
444 for (HighlightSeverity severity : computedSeverities.values()) {
445 if (!severity.equals(ScopesAndSeveritiesTable.MIXED_FAKE_SEVERITY)) {
446 allScopesHasMixedSeverity = false;
450 return allScopesHasMixedSeverity
451 ? ScopesAndSeveritiesTable.MIXED_FAKE_LEVEL.getIcon()
452 : new MultiScopeSeverityIcon(computedSeverities, myDefaultScopeName, inspectionProfile);
456 private Map<String, HighlightSeverity> computeSeverities(final InspectionProfileImpl inspectionProfile) {
457 if (myScopeToAverageSeverityMap.isEmpty()) {
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) {
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);
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) {
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);
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);
508 result.put(currentScope, currentSeverity);
515 public void put(@NotNull final ScopeToolState defaultState, @NotNull final List<ScopeToolState> nonDefault) {
516 putOne(defaultState);
517 if (myDefaultScopeName == null) {
518 myDefaultScopeName = defaultState.getScopeName();
520 for (final ScopeToolState scopeToolState : nonDefault) {
521 putOne(scopeToolState);
525 private void putOne(final ScopeToolState state) {
526 if (!state.isEnabled()) {
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()));
537 severityAndOccurrences.incOccurrences(inspectionName, state.getLevel().getSeverity());