From 277979d3f34118f9264fff2c04f91213e8f893c8 Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Tue, 20 Jul 2021 16:41:42 +0200 Subject: [PATCH] SLI-585 Rework SonarLint tool window layout * Splitter layout will adapt depending on the docking position * Default split value is 50% * Removed useless borders on some panels * Splitter now have a 1px border to "grab" --- .../intellij/ui/AbstractIssuesPanel.java | 46 +- .../ui/SonarLintAnalysisResultsPanel.java | 4 +- .../ui/SonarLintHotspotDescriptionPanel.java | 1 - .../intellij/ui/SonarLintHotspotsPanel.java | 36 +- .../intellij/ui/SonarLintIssuesPanel.java | 19 +- .../intellij/ui/SonarLintRulePanel.java | 1 - .../ui/SonarLintToolWindowFactory.java | 52 +- .../TaintVulnerabilitiesPanel.kt | 520 +++++++++--------- 8 files changed, 354 insertions(+), 325 deletions(-) diff --git a/src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java b/src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java index 0cc976abc9..dffcb4f72b 100644 --- a/src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java +++ b/src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java @@ -20,7 +20,6 @@ package org.sonarlint.intellij.ui; import com.intellij.ide.OccurenceNavigator; -import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionToolbar; @@ -29,12 +28,12 @@ import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.SimpleToolWindowPanel; -import com.intellij.openapi.ui.Splitter; import com.intellij.tools.SimpleActionGroup; import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.components.JBTabbedPane; import com.intellij.ui.treeStructure.Tree; import com.intellij.util.ui.tree.TreeUtil; + import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; @@ -43,7 +42,6 @@ import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.swing.Box; -import javax.swing.JComponent; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; import javax.swing.tree.DefaultMutableTreeNode; @@ -51,6 +49,7 @@ import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; + import org.sonarlint.intellij.editor.EditorDecorator; import org.sonarlint.intellij.issue.LiveIssue; import org.sonarlint.intellij.ui.nodes.AbstractNode; @@ -89,15 +88,15 @@ public void refreshToolbar() { private void createTabs() { // Flows panel with tree - JScrollPane flowsPanel = ScrollPaneFactory.createScrollPane(flowsTree); + JScrollPane flowsPanel = ScrollPaneFactory.createScrollPane(flowsTree, true); flowsPanel.getVerticalScrollBar().setUnitIncrement(10); // Rule panel rulePanel = new SonarLintRulePanel(project); - JScrollPane scrollableRulePanel = ScrollPaneFactory.createScrollPane( - rulePanel.getPanel(), - ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + JScrollPane scrollableRulePanel = ScrollPaneFactory.createScrollPane(rulePanel.getPanel(), true); + + scrollableRulePanel.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scrollableRulePanel.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); scrollableRulePanel.getVerticalScrollBar().setUnitIncrement(10); detailsTab = new JBTabbedPane(); @@ -105,20 +104,6 @@ private void createTabs() { detailsTab.insertTab("Locations", null, flowsPanel, "All locations involved in the issue", LOCATIONS_TAB_INDEX); } - protected JComponent createSplitter(JComponent c1, JComponent c2, String proportionProperty, boolean vertical, float defaultSplit) { - float savedProportion = PropertiesComponent.getInstance(project).getFloat(proportionProperty, defaultSplit); - - final Splitter splitter = new Splitter(vertical); - splitter.setFirstComponent(c1); - splitter.setSecondComponent(c2); - splitter.setProportion(savedProportion); - splitter.setHonorComponentsMinimumSize(true); - splitter.addPropertyChangeListener(Splitter.PROP_PROPORTION, - evt -> PropertiesComponent.getInstance(project).setValue(proportionProperty, Float.toString(splitter.getProportion()))); - - return splitter; - } - protected void issueTreeSelectionChanged() { IssueNode[] selectedNodes = tree.getSelectedNodes(IssueNode.class, null); if (selectedNodes.length > 0) { @@ -184,7 +169,8 @@ public void keyPressed(KeyEvent e) { } }); tree.addFocusListener(new FocusAdapter() { - @Override public void focusGained(FocusEvent e) { + @Override + public void focusGained(FocusEvent e) { if (!e.isTemporary()) { issueTreeSelectionChanged(); } @@ -211,7 +197,8 @@ private OccurenceNavigator.OccurenceInfo occurrence(@Nullable IssueNode node) { -1); } - @Override public boolean hasNextOccurence() { + @Override + public boolean hasNextOccurence() { // relies on the assumption that a TreeNodes will always be the last row in the table view of the tree TreePath path = tree.getSelectionPath(); if (path == null) { @@ -225,7 +212,8 @@ private OccurenceNavigator.OccurenceInfo occurrence(@Nullable IssueNode node) { } } - @Override public boolean hasPreviousOccurence() { + @Override + public boolean hasPreviousOccurence() { TreePath path = tree.getSelectionPath(); if (path == null) { return false; @@ -259,18 +247,20 @@ public OccurenceNavigator.OccurenceInfo goPreviousOccurence() { return occurrence(treeBuilder.getPreviousIssue((AbstractNode) path.getLastPathComponent())); } - @Override public String getNextOccurenceActionName() { + @Override + public String getNextOccurenceActionName() { return "Next Issue"; } - @Override public String getPreviousOccurenceActionName() { + @Override + public String getPreviousOccurenceActionName() { return "Previous Issue"; } public void setSelectedIssue(LiveIssue issue) { DefaultMutableTreeNode issueNode = TreeUtil.findNode(((DefaultMutableTreeNode) tree.getModel().getRoot()), (node) -> node instanceof IssueNode && ((IssueNode) node).issue().equals(issue)); - if(issueNode == null) { + if (issueNode == null) { return; } tree.setSelectionPath(null); diff --git a/src/main/java/org/sonarlint/intellij/ui/SonarLintAnalysisResultsPanel.java b/src/main/java/org/sonarlint/intellij/ui/SonarLintAnalysisResultsPanel.java index 49e0084322..6d7bb23d20 100644 --- a/src/main/java/org/sonarlint/intellij/ui/SonarLintAnalysisResultsPanel.java +++ b/src/main/java/org/sonarlint/intellij/ui/SonarLintAnalysisResultsPanel.java @@ -32,6 +32,8 @@ import org.sonarlint.intellij.messages.StatusListener; import org.sonarlint.intellij.util.SonarLintActions; +import static org.sonarlint.intellij.ui.SonarLintToolWindowFactory.createSplitter; + public class SonarLintAnalysisResultsPanel extends AbstractIssuesPanel implements Disposable { private static final String SPLIT_PROPORTION_PROPERTY = "SONARLINT_ANALYSIS_RESULTS_SPLIT_PROPORTION"; @@ -50,7 +52,7 @@ public SonarLintAnalysisResultsPanel(Project project) { setToolbar(createActionGroup()); // Put everything together - super.setContent(createSplitter(issuesPanel, detailsTab, SPLIT_PROPORTION_PROPERTY, false, 0.65f)); + super.setContent(createSplitter(project, this, this, issuesPanel, detailsTab, SPLIT_PROPORTION_PROPERTY, 0.5f)); // Subscribe to events subscribeToEvents(); diff --git a/src/main/java/org/sonarlint/intellij/ui/SonarLintHotspotDescriptionPanel.java b/src/main/java/org/sonarlint/intellij/ui/SonarLintHotspotDescriptionPanel.java index 783867b745..86d7278df6 100644 --- a/src/main/java/org/sonarlint/intellij/ui/SonarLintHotspotDescriptionPanel.java +++ b/src/main/java/org/sonarlint/intellij/ui/SonarLintHotspotDescriptionPanel.java @@ -58,7 +58,6 @@ public SonarLintHotspotDescriptionPanel(Project project) { styleSheet.addRule("td.pad {padding: 0px 10px 0px 0px;}"); panel = new JPanel(new BorderLayout()); - panel.setBorder(IdeBorderFactory.createBorder(SideBorder.LEFT)); JComponent titleComp = new JLabel("Select a hotspot to see more details", SwingConstants.CENTER); panel.add(titleComp, BorderLayout.CENTER); diff --git a/src/main/java/org/sonarlint/intellij/ui/SonarLintHotspotsPanel.java b/src/main/java/org/sonarlint/intellij/ui/SonarLintHotspotsPanel.java index 515afe7674..cd6fbc925e 100644 --- a/src/main/java/org/sonarlint/intellij/ui/SonarLintHotspotsPanel.java +++ b/src/main/java/org/sonarlint/intellij/ui/SonarLintHotspotsPanel.java @@ -19,19 +19,22 @@ */ package org.sonarlint.intellij.ui; -import com.intellij.ide.util.PropertiesComponent; +import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.SimpleToolWindowPanel; -import com.intellij.openapi.ui.Splitter; import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.components.JBTabbedPane; + import javax.swing.JComponent; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; + import org.sonarlint.intellij.issue.hotspot.LocalHotspot; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; -public class SonarLintHotspotsPanel extends SimpleToolWindowPanel { +import static org.sonarlint.intellij.ui.SonarLintToolWindowFactory.createSplitter; + +public class SonarLintHotspotsPanel extends SimpleToolWindowPanel implements Disposable { private static final String SPLIT_PROPORTION_PROPERTY = "SONARLINT_HOTSPOTS_SPLIT_PROPORTION"; private static final float DEFAULT_SPLIT_PROPORTION = 0.5f; @@ -58,32 +61,17 @@ public SonarLintHotspotsPanel(Project project) { hotspotDetailsTab.addTab("Details", null, scrollable(detailsPanel.getPanel()), "Details about the hotspot"); hotspotDetailsTab.setVisible(false); - super.setContent(createSplitter(hotspotsListPanel.getPanel(), hotspotDetailsTab, SPLIT_PROPORTION_PROPERTY, project)); + super.setContent(createSplitter(project, this, this, hotspotsListPanel.getPanel(), hotspotDetailsTab, SPLIT_PROPORTION_PROPERTY, DEFAULT_SPLIT_PROPORTION)); } private static JScrollPane scrollable(JComponent component) { - JScrollPane scrollableRulePanel = ScrollPaneFactory.createScrollPane( - component, - ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + JScrollPane scrollableRulePanel = ScrollPaneFactory.createScrollPane(component, true); + scrollableRulePanel.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scrollableRulePanel.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); scrollableRulePanel.getVerticalScrollBar().setUnitIncrement(10); return scrollableRulePanel; } - protected JComponent createSplitter(JComponent c1, JComponent c2, String proportionProperty, Project project) { - float savedProportion = PropertiesComponent.getInstance(project).getFloat(proportionProperty, DEFAULT_SPLIT_PROPORTION); - - final Splitter splitter = new Splitter(false); - splitter.setFirstComponent(c1); - splitter.setSecondComponent(c2); - splitter.setProportion(savedProportion); - splitter.setHonorComponentsMinimumSize(true); - splitter.addPropertyChangeListener(Splitter.PROP_PROPORTION, - evt -> PropertiesComponent.getInstance(project).setValue(proportionProperty, Float.toString(splitter.getProportion()))); - - return splitter; - } - public void setHotspot(LocalHotspot hotspot) { hotspotDetailsTab.setVisible(true); hotspotsListPanel.setHotspot(hotspot); @@ -94,4 +82,8 @@ public void setHotspot(LocalHotspot hotspot) { detailsPanel.setDetails(hotspot); } + @Override + public void dispose() { + // Nothing to do + } } diff --git a/src/main/java/org/sonarlint/intellij/ui/SonarLintIssuesPanel.java b/src/main/java/org/sonarlint/intellij/ui/SonarLintIssuesPanel.java index 6485a0f4fc..bb68e915a7 100644 --- a/src/main/java/org/sonarlint/intellij/ui/SonarLintIssuesPanel.java +++ b/src/main/java/org/sonarlint/intellij/ui/SonarLintIssuesPanel.java @@ -19,14 +19,16 @@ */ package org.sonarlint.intellij.ui; +import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.actionSystem.DataProvider; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.JBSplitter; import com.intellij.ui.ScrollPaneFactory; import com.intellij.util.ui.tree.TreeUtil; + import java.awt.BorderLayout; import java.awt.EventQueue; import java.util.ArrayList; @@ -35,14 +37,18 @@ import java.util.List; import javax.annotation.Nullable; import javax.swing.JPanel; + import org.jetbrains.annotations.NonNls; import org.sonarlint.intellij.issue.LiveIssue; import org.sonarlint.intellij.util.SonarLintActions; import org.sonarlint.intellij.util.SonarLintUtils; -public class SonarLintIssuesPanel extends AbstractIssuesPanel implements DataProvider { +import static org.sonarlint.intellij.ui.SonarLintToolWindowFactory.createSplitter; + +public class SonarLintIssuesPanel extends AbstractIssuesPanel implements Disposable { private static final String SPLIT_PROPORTION_PROPERTY = "SONARLINT_ISSUES_SPLIT_PROPORTION"; private final CurrentFileController scope; + private final JBSplitter splitter; public SonarLintIssuesPanel(Project project, CurrentFileController scope) { super(project); @@ -54,11 +60,16 @@ public SonarLintIssuesPanel(Project project, CurrentFileController scope) { issuesPanel.add(ScrollPaneFactory.createScrollPane(tree), BorderLayout.CENTER); issuesPanel.add(new AutoTriggerStatusPanel(project).getPanel(), BorderLayout.SOUTH); - super.setContent(createSplitter(issuesPanel, detailsTab, SPLIT_PROPORTION_PROPERTY, false, 0.65f)); - + splitter = createSplitter(project, this, this, issuesPanel, detailsTab, SPLIT_PROPORTION_PROPERTY, 0.5f); + super.setContent(splitter); subscribeToEvents(); } + @Override + public void dispose() { + // Nothing to do + } + private static Collection actions() { SonarLintActions sonarLintActions = SonarLintActions.getInstance(); List list = new ArrayList<>(); diff --git a/src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.java b/src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.java index 1c03870ca9..9db16b6233 100644 --- a/src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.java +++ b/src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.java @@ -67,7 +67,6 @@ public class SonarLintRulePanel { public SonarLintRulePanel(Project project) { this.project = project; panel = new JPanel(new BorderLayout()); - panel.setBorder(IdeBorderFactory.createBorder(SideBorder.LEFT)); setRuleKey(null); show(); } diff --git a/src/main/java/org/sonarlint/intellij/ui/SonarLintToolWindowFactory.java b/src/main/java/org/sonarlint/intellij/ui/SonarLintToolWindowFactory.java index e8d786ada4..07d782b7f1 100644 --- a/src/main/java/org/sonarlint/intellij/ui/SonarLintToolWindowFactory.java +++ b/src/main/java/org/sonarlint/intellij/ui/SonarLintToolWindowFactory.java @@ -19,13 +19,22 @@ */ package org.sonarlint.intellij.ui; +import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowAnchor; import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.openapi.wm.ToolWindowType; +import com.intellij.openapi.wm.ex.ToolWindowManagerListener; +import com.intellij.ui.JBSplitter; +import com.intellij.ui.OnePixelSplitter; import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentManager; + +import javax.swing.JComponent; + import org.jetbrains.annotations.NotNull; import org.sonarlint.intellij.actions.SonarLintToolWindow; import org.sonarlint.intellij.issue.IssueManager; @@ -65,6 +74,40 @@ public void createToolWindowContent(Project project, final ToolWindow toolWindow } } + public static JBSplitter createSplitter(Project project, JComponent parentComponent, Disposable parentDisposable, JComponent c1, JComponent c2, String proportionProperty, + float defaultSplit) { + JBSplitter splitter = new OnePixelSplitter(splitVertically(project), proportionProperty, defaultSplit); + splitter.setFirstComponent(c1); + splitter.setSecondComponent(c2); + splitter.setHonorComponentsMinimumSize(true); + + final ToolWindowManagerListener listener = new ToolWindowManagerListener() { + @Override + public void stateChanged(@NotNull ToolWindowManager toolWindowManager) { + splitter.setOrientation(splitVertically(project)); + parentComponent.revalidate(); + parentComponent.repaint(); + } + }; + project.getMessageBus().connect(parentDisposable).subscribe(ToolWindowManagerListener.TOPIC, listener); + Disposer.register(parentDisposable, () -> { + parentComponent.remove(splitter); + splitter.dispose(); + }); + + return splitter; + } + + public static boolean splitVertically(Project project) { + final ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow(SonarLintToolWindowFactory.TOOL_WINDOW_ID); + boolean splitVertically = false; + if (toolWindow != null) { + final ToolWindowAnchor anchor = toolWindow.getAnchor(); + splitVertically = anchor == ToolWindowAnchor.LEFT || anchor == ToolWindowAnchor.RIGHT; + } + return splitVertically; + } + private static void addIssuesTab(Project project, @NotNull ContentManager contentManager) { IssueManager issueManager = getService(project, IssueManager.class); CurrentFileController scope = new CurrentFileController(project, issueManager); @@ -95,16 +138,15 @@ private static void addAnalysisResultsTab(Project project, @NotNull ContentManag private static void addTaintIssuesTab(Project project, @NotNull ContentManager contentManager) { TaintVulnerabilitiesPanel vulnerabilitiesPanel = new TaintVulnerabilitiesPanel(project); Content analysisResultsContent = contentManager.getFactory() - .createContent( - vulnerabilitiesPanel, - buildVulnerabilitiesTabName(0), - false); + .createContent( + vulnerabilitiesPanel, + buildVulnerabilitiesTabName(0), + false); analysisResultsContent.setCloseable(false); contentManager.addDataProvider(vulnerabilitiesPanel); contentManager.addContent(analysisResultsContent); } - private static void addLogTab(Project project, ToolWindow toolWindow) { Content logContent = toolWindow.getContentManager().getFactory() .createContent( diff --git a/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/TaintVulnerabilitiesPanel.kt b/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/TaintVulnerabilitiesPanel.kt index 69ccfbc198..a56d35d002 100644 --- a/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/TaintVulnerabilitiesPanel.kt +++ b/src/main/java/org/sonarlint/intellij/ui/vulnerabilities/TaintVulnerabilitiesPanel.kt @@ -22,7 +22,7 @@ package org.sonarlint.intellij.ui.vulnerabilities import com.intellij.icons.AllIcons.General.Information import com.intellij.ide.OccurenceNavigator import com.intellij.ide.OccurenceNavigator.OccurenceInfo -import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent @@ -30,7 +30,6 @@ import com.intellij.openapi.actionSystem.DataProvider import com.intellij.openapi.fileEditor.OpenFileDescriptor import com.intellij.openapi.project.Project import com.intellij.openapi.ui.SimpleToolWindowPanel -import com.intellij.openapi.ui.Splitter import com.intellij.tools.SimpleActionGroup import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.components.JBTabbedPane @@ -44,8 +43,13 @@ import org.sonarlint.intellij.actions.SonarConfigureProject import org.sonarlint.intellij.config.Settings.getGlobalSettings import org.sonarlint.intellij.core.ProjectBindingManager import org.sonarlint.intellij.editor.EditorDecorator -import org.sonarlint.intellij.issue.vulnerabilities.* +import org.sonarlint.intellij.issue.vulnerabilities.FoundTaintVulnerabilities +import org.sonarlint.intellij.issue.vulnerabilities.InvalidBinding +import org.sonarlint.intellij.issue.vulnerabilities.LocalTaintVulnerability +import org.sonarlint.intellij.issue.vulnerabilities.NoBinding +import org.sonarlint.intellij.issue.vulnerabilities.TaintVulnerabilitiesStatus import org.sonarlint.intellij.ui.SonarLintRulePanel +import org.sonarlint.intellij.ui.SonarLintToolWindowFactory.createSplitter import org.sonarlint.intellij.ui.nodes.AbstractNode import org.sonarlint.intellij.ui.nodes.IssueNode import org.sonarlint.intellij.ui.nodes.LocalTaintVulnerabilityNode @@ -57,8 +61,12 @@ import java.awt.event.FocusAdapter import java.awt.event.FocusEvent import java.awt.event.KeyAdapter import java.awt.event.KeyEvent -import java.util.* -import javax.swing.* +import javax.swing.Box +import javax.swing.BoxLayout +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.ScrollPaneConstants import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.TreeNode import javax.swing.tree.TreePath @@ -66,7 +74,7 @@ import javax.swing.tree.TreeSelectionModel private const val SPLIT_PROPORTION_PROPERTY = "SONARLINT_TAINT_VULNERABILITIES_SPLIT_PROPORTION" -private const val DEFAULT_SPLIT_PROPORTION = 0.65f +private const val DEFAULT_SPLIT_PROPORTION = 0.5f private const val NO_BINDING_CARD_ID = "NO_BINDING_CARD" private const val INVALID_BINDING_CARD_ID = "INVALID_BINDING_CARD" @@ -76,282 +84,268 @@ private const val TREE_CARD_ID = "TREE_CARD" private const val TOOLBAR_GROUP_ID = "TaintVulnerabilities" class TaintVulnerabilitiesPanel(private val project: Project) : SimpleToolWindowPanel(false, true), - OccurenceNavigator, DataProvider { + OccurenceNavigator, DataProvider, Disposable { - private lateinit var rulePanel: SonarLintRulePanel - private lateinit var tree: TaintVulnerabilityTree - private lateinit var treeBuilder: TaintVulnerabilityTreeModelBuilder - private val noVulnerabilitiesLabel = JLabel("") - private val cards = CardPanel() + private lateinit var rulePanel: SonarLintRulePanel + private lateinit var tree: TaintVulnerabilityTree + private lateinit var treeBuilder: TaintVulnerabilityTreeModelBuilder + private val noVulnerabilitiesLabel = JLabel("") + private val cards = CardPanel() - class CardPanel { - val container = JPanel() - private var subPanels = mutableMapOf() + class CardPanel { + val container = JPanel() + private var subPanels = mutableMapOf() + + init { + container.layout = BoxLayout(container, BoxLayout.PAGE_AXIS) + } + + fun add(panel: JComponent, id: String) { + panel.isVisible = subPanels.isEmpty() + panel.alignmentX = 0.5f + panel.alignmentY = 0.5f + container.add(panel) + subPanels[id] = panel + } + + fun show(id: String) { + subPanels.values.forEach { it.isVisible = false } + subPanels[id]!!.isVisible = true + } + } init { - container.layout = BoxLayout(container, BoxLayout.PAGE_AXIS) + cards.add(centeredLabel("The project is not bound to SonarQube/SonarCloud", ActionLink("Configure Binding", SonarConfigureProject())), NO_BINDING_CARD_ID) + cards.add(centeredLabel("The project binding is invalid", ActionLink("Edit Binding", SonarConfigureProject())), INVALID_BINDING_CARD_ID) + cards.add(centeredLabel(noVulnerabilitiesLabel), NO_ISSUES_CARD_ID) + cards.add(createSplitter(project, this, this, ScrollPaneFactory.createScrollPane(createTree()), createRulePanel(), SPLIT_PROPORTION_PROPERTY, DEFAULT_SPLIT_PROPORTION), + TREE_CARD_ID + ) + val issuesPanel = JPanel(BorderLayout()) + val globalSettings = getGlobalSettings() + if (!globalSettings.isTaintVulnerabilitiesTabDisclaimerDismissed) { + issuesPanel.add(createDisclaimer(), BorderLayout.NORTH) + } + issuesPanel.add(cards.container, BorderLayout.CENTER) + setContent(issuesPanel) + setupToolbar(listOf(RefreshTaintVulnerabilitiesAction(), OpenTaintVulnerabilityDocumentationAction())) + } + + private fun centeredLabel(text: String, actionLink: ActionLink? = null) = centeredLabel(JLabel(text), actionLink) + + private fun centeredLabel(textLabel: JLabel, actionLink: ActionLink? = null): JPanel { + val labelPanel = JPanel(HorizontalLayout(5)) + labelPanel.add(textLabel, HorizontalLayout.CENTER) + if (actionLink != null) labelPanel.add(actionLink, HorizontalLayout.CENTER) + return labelPanel } - fun add(panel: JComponent, id: String) { - panel.isVisible = subPanels.isEmpty() - panel.alignmentX = 0.5f - panel.alignmentY = 0.5f - container.add(panel) - subPanels[id] = panel + private fun expandTree() { + if (tree.getSelectedNode() == null) { + if (treeBuilder.numberIssues() < 30) { + TreeUtil.expand(tree, 2) + } else { + tree.expandRow(0) + } + } } - fun show(id: String) { - subPanels.values.forEach { it.isVisible = false } - subPanels[id]!!.isVisible = true + private fun createDisclaimer(): StripePanel { + val stripePanel = StripePanel("This tab displays taint vulnerabilities detected by SonarQube or SonarCloud. SonarLint does not detect those issues locally.", Information) + stripePanel.addAction("Learn more", OpenTaintVulnerabilityDocumentationAction()) + stripePanel.addAction("Dismiss", object : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + stripePanel.isVisible = false + getGlobalSettings().dismissTaintVulnerabilitiesTabDisclaimer() + } + }) + return stripePanel } - } - - init { - cards.add(centeredLabel("The project is not bound to SonarQube/SonarCloud", ActionLink("Configure Binding", SonarConfigureProject())), NO_BINDING_CARD_ID) - cards.add(centeredLabel("The project binding is invalid", ActionLink("Edit Binding", SonarConfigureProject())), INVALID_BINDING_CARD_ID) - cards.add(centeredLabel(noVulnerabilitiesLabel), NO_ISSUES_CARD_ID) - cards.add(createSplitter( - ScrollPaneFactory.createScrollPane(createTree()), - createRulePanel()), - TREE_CARD_ID - ) - val issuesPanel = JPanel(BorderLayout()) - val globalSettings = getGlobalSettings() - if (!globalSettings.isTaintVulnerabilitiesTabDisclaimerDismissed) { - issuesPanel.add(createDisclaimer(), BorderLayout.NORTH) + + private fun setupToolbar(actions: List) { + val group = SimpleActionGroup() + actions.forEach { group.add(it) } + val toolbar = ActionManager.getInstance().createActionToolbar(TOOLBAR_GROUP_ID, group, false) + toolbar.setTargetComponent(this) + val toolBarBox = Box.createHorizontalBox() + toolBarBox.add(toolbar.component) + setToolbar(toolBarBox) + toolbar.component.isVisible = true } - issuesPanel.add(cards.container, BorderLayout.CENTER) - setContent(issuesPanel) - setupToolbar(listOf(RefreshTaintVulnerabilitiesAction(), OpenTaintVulnerabilityDocumentationAction())) - } - - private fun centeredLabel(text: String, actionLink: ActionLink? = null) = centeredLabel(JLabel(text), actionLink) - - private fun centeredLabel(textLabel: JLabel, actionLink: ActionLink? = null): JPanel { - val labelPanel = JPanel(HorizontalLayout(5)) - labelPanel.add(textLabel, HorizontalLayout.CENTER) - if (actionLink != null) labelPanel.add(actionLink, HorizontalLayout.CENTER) - return labelPanel - } - - private fun expandTree() { - if (tree.getSelectedNode() == null) { - if (treeBuilder.numberIssues() < 30) { - TreeUtil.expand(tree, 2) - } else { - tree.expandRow(0) - } + + private fun showCard(id: String) { + cards.show(id) } - } - - private fun createDisclaimer(): StripePanel { - val stripePanel = StripePanel("This tab displays taint vulnerabilities detected by SonarQube or SonarCloud. SonarLint does not detect those issues locally.", Information) - stripePanel.addAction("Learn more", OpenTaintVulnerabilityDocumentationAction()) - stripePanel.addAction("Dismiss", object : AnAction() { - override fun actionPerformed(e: AnActionEvent) { - stripePanel.isVisible = false - getGlobalSettings().dismissTaintVulnerabilitiesTabDisclaimer() - } - }) - return stripePanel - } - - private fun setupToolbar(actions: List) { - val group = SimpleActionGroup() - actions.forEach { group.add(it) } - val toolbar = ActionManager.getInstance().createActionToolbar(TOOLBAR_GROUP_ID, group, false) - toolbar.setTargetComponent(this) - val toolBarBox = Box.createHorizontalBox() - toolBarBox.add(toolbar.component) - setToolbar(toolBarBox) - toolbar.component.isVisible = true - } - - private fun showCard(id: String) { - cards.show(id) - } - - fun populate(status: TaintVulnerabilitiesStatus) { - val highlighting = getService(project, EditorDecorator::class.java) - when (status) { - is NoBinding -> { - showCard(NO_BINDING_CARD_ID) - highlighting.removeHighlights() - } - is InvalidBinding -> { - showCard(INVALID_BINDING_CARD_ID) - highlighting.removeHighlights() - } - is FoundTaintVulnerabilities -> { - if (status.isEmpty()) { - showNoVulnerabilitiesLabel() - highlighting.removeHighlights() - } else { - showCard(TREE_CARD_ID) - val selectionPath: TreePath? = tree.selectionPath - val expandedPaths = getExpandedPaths() - treeBuilder.updateModel(status.byFile) - tree.selectionPath = selectionPath - expandedPaths.forEach { tree.expandPath(it) } - expandTree() + + fun populate(status: TaintVulnerabilitiesStatus) { + val highlighting = getService(project, EditorDecorator::class.java) + when (status) { + is NoBinding -> { + showCard(NO_BINDING_CARD_ID) + highlighting.removeHighlights() + } + is InvalidBinding -> { + showCard(INVALID_BINDING_CARD_ID) + highlighting.removeHighlights() + } + is FoundTaintVulnerabilities -> { + if (status.isEmpty()) { + showNoVulnerabilitiesLabel() + highlighting.removeHighlights() + } else { + showCard(TREE_CARD_ID) + val selectionPath: TreePath? = tree.selectionPath + val expandedPaths = getExpandedPaths() + treeBuilder.updateModel(status.byFile) + tree.selectionPath = selectionPath + expandedPaths.forEach { tree.expandPath(it) } + expandTree() + } + } } - } } - } - - private fun showNoVulnerabilitiesLabel() { - val serverConnection = getService(project, ProjectBindingManager::class.java).serverConnection - noVulnerabilitiesLabel.text = "No vulnerabilities found for currently opened files in the latest analysis on ${serverConnection.productName}." - showCard(NO_ISSUES_CARD_ID) - } - - private fun getExpandedPaths(): List { - val expanded: MutableList = ArrayList() - for (i in 0 until tree.rowCount - 1) { - val currPath: TreePath = tree.getPathForRow(i) - val nextPath: TreePath = tree.getPathForRow(i + 1) - if (currPath.isDescendant(nextPath)) { - expanded.add(currPath) - } + + private fun showNoVulnerabilitiesLabel() { + val serverConnection = getService(project, ProjectBindingManager::class.java).serverConnection + noVulnerabilitiesLabel.text = "No vulnerabilities found for currently opened files in the latest analysis on ${serverConnection.productName}." + showCard(NO_ISSUES_CARD_ID) + } + + private fun getExpandedPaths(): List { + val expanded: MutableList = ArrayList() + for (i in 0 until tree.rowCount - 1) { + val currPath: TreePath = tree.getPathForRow(i) + val nextPath: TreePath = tree.getPathForRow(i + 1) + if (currPath.isDescendant(nextPath)) { + expanded.add(currPath) + } + } + return expanded } - return expanded - } - - fun setSelectedVulnerability(vulnerability: LocalTaintVulnerability) { - val vulnerabilityNode = TreeUtil.findNode(tree.model.root as DefaultMutableTreeNode) - { it is LocalTaintVulnerabilityNode && it.issue.key() == vulnerability.key() } - ?: return - tree.selectionPath = null - tree.addSelectionPath(TreePath(vulnerabilityNode.path)) - } - - private fun createRulePanel(): JBTabbedPane { - // Rule panel - rulePanel = SonarLintRulePanel(project) - val scrollableRulePanel = ScrollPaneFactory.createScrollPane( - rulePanel.panel, - ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER - ) - scrollableRulePanel.verticalScrollBar.unitIncrement = 10 - val detailsTab = JBTabbedPane() - detailsTab.addTab("Rule", null, scrollableRulePanel, "Details about the rule") - return detailsTab - } - - private fun createSplitter(c1: JComponent, c2: JComponent): JComponent { - val savedProportion = PropertiesComponent.getInstance(project).getFloat(SPLIT_PROPORTION_PROPERTY, DEFAULT_SPLIT_PROPORTION) - val splitter = Splitter(false) - splitter.firstComponent = c1 - splitter.secondComponent = c2 - splitter.proportion = savedProportion - splitter.setHonorComponentsMinimumSize(true) - splitter.addPropertyChangeListener(Splitter.PROP_PROPORTION) { - PropertiesComponent.getInstance(project).setValue( - SPLIT_PROPORTION_PROPERTY, splitter.proportion.toString() - ) + + fun setSelectedVulnerability(vulnerability: LocalTaintVulnerability) { + val vulnerabilityNode = TreeUtil.findNode(tree.model.root as DefaultMutableTreeNode) + { it is LocalTaintVulnerabilityNode && it.issue.key() == vulnerability.key() } + ?: return + tree.selectionPath = null + tree.addSelectionPath(TreePath(vulnerabilityNode.path)) } - return splitter - } - - private fun updateRulePanelContent() { - val highlighting = getService(project, EditorDecorator::class.java) - val issue = tree.getIssueFromSelectedNode() - if (issue == null) { - rulePanel.setRuleKey(null) - highlighting.removeHighlights() - } else { - rulePanel.setRuleKey(issue.ruleKey()) - highlighting.highlight(issue) + + private fun createRulePanel(): JBTabbedPane { + // Rule panel + rulePanel = SonarLintRulePanel(project) + + val scrollableRulePanel = ScrollPaneFactory.createScrollPane(rulePanel.panel, true) + scrollableRulePanel.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER) + scrollableRulePanel.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED) + scrollableRulePanel.verticalScrollBar.unitIncrement = 10 + val detailsTab = JBTabbedPane() + detailsTab.addTab("Rule", null, scrollableRulePanel, "Details about the rule") + return detailsTab } - } - - private fun createTree(): TaintVulnerabilityTree { - treeBuilder = TaintVulnerabilityTreeModelBuilder() - tree = TaintVulnerabilityTree(project, treeBuilder.model) - tree.addTreeSelectionListener { updateRulePanelContent() } - tree.addKeyListener(object : KeyAdapter() { - override fun keyPressed(e: KeyEvent) { - if (KeyEvent.VK_ESCAPE == e.keyCode) { - val highlighting = getService( - project, - EditorDecorator::class.java - ) - highlighting.removeHighlights() + + private fun updateRulePanelContent() { + val highlighting = getService(project, EditorDecorator::class.java) + val issue = tree.getIssueFromSelectedNode() + if (issue == null) { + rulePanel.setRuleKey(null) + highlighting.removeHighlights() + } else { + rulePanel.setRuleKey(issue.ruleKey()) + highlighting.highlight(issue) } - } - }) - tree.addFocusListener(object : FocusAdapter() { - override fun focusGained(e: FocusEvent) { - if (!e.isTemporary) { - updateRulePanelContent() + } + + private fun createTree(): TaintVulnerabilityTree { + treeBuilder = TaintVulnerabilityTreeModelBuilder() + tree = TaintVulnerabilityTree(project, treeBuilder.model) + tree.addTreeSelectionListener { updateRulePanelContent() } + tree.addKeyListener(object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + if (KeyEvent.VK_ESCAPE == e.keyCode) { + val highlighting = getService( + project, + EditorDecorator::class.java + ) + highlighting.removeHighlights() + } + } + }) + tree.addFocusListener(object : FocusAdapter() { + override fun focusGained(e: FocusEvent) { + if (!e.isTemporary) { + updateRulePanelContent() + } + } + }) + tree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION + return tree + } + + private fun occurrence(node: IssueNode?): OccurenceInfo? { + if (node == null) { + return null + } + val path = TreePath(node.path) + tree.selectionModel.selectionPath = path + tree.scrollPathToVisible(path) + val range = node.issue().range + val startOffset = range?.startOffset ?: 0 + return OccurenceInfo( + OpenFileDescriptor(project, node.issue().psiFile().virtualFile, startOffset), + -1, + -1 + ) + } + + override fun hasNextOccurence(): Boolean { + // relies on the assumption that a TreeNodes will always be the last row in the table view of the tree + val path = tree.selectionPath ?: return false + val node = path.lastPathComponent as DefaultMutableTreeNode + return if (node is IssueNode) { + tree.rowCount != tree.getRowForPath(path) + 1 + } else { + node.childCount > 0 } - } - }) - tree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION - return tree - } - - private fun occurrence(node: IssueNode?): OccurenceInfo? { - if (node == null) { - return null } - val path = TreePath(node.path) - tree.selectionModel.selectionPath = path - tree.scrollPathToVisible(path) - val range = node.issue().range - val startOffset = range?.startOffset ?: 0 - return OccurenceInfo( - OpenFileDescriptor(project, node.issue().psiFile().virtualFile, startOffset), - -1, - -1 - ) - } - - override fun hasNextOccurence(): Boolean { - // relies on the assumption that a TreeNodes will always be the last row in the table view of the tree - val path = tree.selectionPath ?: return false - val node = path.lastPathComponent as DefaultMutableTreeNode - return if (node is IssueNode) { - tree.rowCount != tree.getRowForPath(path) + 1 - } else { - node.childCount > 0 + + override fun hasPreviousOccurence(): Boolean { + val path = tree.selectionPath ?: return false + val node = path.lastPathComponent as DefaultMutableTreeNode + return node is IssueNode && !isFirst(node) + } + + private fun isFirst(node: TreeNode): Boolean { + val parent = node.parent + return parent == null || parent.getIndex(node) == 0 && isFirst(parent) + } + + override fun goNextOccurence(): OccurenceInfo? { + val path = tree.selectionPath ?: return null + return occurrence(treeBuilder.getNextIssue(path.lastPathComponent as AbstractNode)) + } + + override fun goPreviousOccurence(): OccurenceInfo? { + val path = tree.selectionPath ?: return null + return occurrence(treeBuilder.getPreviousIssue(path.lastPathComponent as AbstractNode)) + } + + override fun getNextOccurenceActionName(): String { + return "Next Issue" + } + + override fun getPreviousOccurenceActionName(): String { + return "Previous Issue" + } + + override fun getData(dataId: String): Any? { + return if (OpenIssueInBrowserAction.TAINT_VULNERABILITY_DATA_KEY.`is`(dataId)) { + tree.getSelectedIssue() + } else null + } + + override fun dispose() { + // Nothing to do } - } - - override fun hasPreviousOccurence(): Boolean { - val path = tree.selectionPath ?: return false - val node = path.lastPathComponent as DefaultMutableTreeNode - return node is IssueNode && !isFirst(node) - } - - private fun isFirst(node: TreeNode): Boolean { - val parent = node.parent - return parent == null || parent.getIndex(node) == 0 && isFirst(parent) - } - - override fun goNextOccurence(): OccurenceInfo? { - val path = tree.selectionPath ?: return null - return occurrence(treeBuilder.getNextIssue(path.lastPathComponent as AbstractNode)) - } - - override fun goPreviousOccurence(): OccurenceInfo? { - val path = tree.selectionPath ?: return null - return occurrence(treeBuilder.getPreviousIssue(path.lastPathComponent as AbstractNode)) - } - - override fun getNextOccurenceActionName(): String { - return "Next Issue" - } - - override fun getPreviousOccurenceActionName(): String { - return "Previous Issue" - } - - override fun getData(dataId: String): Any? { - return if (OpenIssueInBrowserAction.TAINT_VULNERABILITY_DATA_KEY.`is`(dataId)) { - tree.getSelectedIssue() - } else null - } }